Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bb2eb4c36 |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,20 +0,0 @@
|
||||
services:
|
||||
php:
|
||||
image: php:7.4-fpm-alpine
|
||||
volumes:
|
||||
- ./www:/usr/share/nginx/html
|
||||
- ./www/php-fpm/www.conf:/usr/local/etc/php-fpm.d/www.conf
|
||||
command: >
|
||||
sh -c "apk add --no-cache $PHPIZE_DEPS && docker-php-ext-install mysqli pdo_mysql && apk del $PHPIZE_DEPS && php-fpm"
|
||||
|
||||
nginx:
|
||||
image: nginx:stable-alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./www:/usr/share/nginx/html
|
||||
depends_on:
|
||||
- php
|
||||
# 使用nc探测tcp端口,不再用wget(wget不兼容fastcgi)
|
||||
command: ["/bin/sh", "-c", "until nc -z php 9000; do echo '等待php-fpm端口就绪...'; sleep 1; done; nginx -g 'daemon off;'"]
|
||||
@@ -1,23 +0,0 @@
|
||||
services:
|
||||
php:
|
||||
image: php:7.4-fpm-alpine
|
||||
user: "101:101"
|
||||
volumes:
|
||||
- ./www:/usr/share/nginx/html
|
||||
# 关键:覆盖fpm监听配置
|
||||
- ./www/php-fpm/www.conf:/usr/local/etc/php-fpm.d/www.conf
|
||||
# 直接apk安装mysqli,不编译,避免gcc全家桶
|
||||
command: >
|
||||
sh -c "apk add --no-cache php7.4-mysqli && php-fpm"
|
||||
|
||||
nginx:
|
||||
image: nginx:stable-alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./www:/usr/share/nginx/html
|
||||
depends_on:
|
||||
- php
|
||||
# alpine自带wget等待php端口就绪
|
||||
command: ["/bin/sh", "-c", "until wget -q -T1 php:9000; do echo '等待php-fpm就绪...'; sleep 1; done; nginx -g 'daemon off;'"]
|
||||
@@ -1,17 +0,0 @@
|
||||
services:
|
||||
php:
|
||||
image: php:7.4-fpm-alpine
|
||||
volumes:
|
||||
- ./www:/usr/share/nginx/html
|
||||
# 新增安装mysqli扩展
|
||||
command: >
|
||||
sh -c "docker-php-ext-install mysqli && docker-php-ext-enable mysqli && php-fpm"
|
||||
nginx:
|
||||
image: nginx:stable-alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./www:/usr/share/nginx/html
|
||||
depends_on:
|
||||
- php
|
||||
@@ -1,54 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm index.php;
|
||||
|
||||
# 开启Gzip全局压缩(文本类压缩60%-80%)
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||
gzip_vary on;
|
||||
|
||||
# PHP统一解析 + 优化参数
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
|
||||
# PHP-FPM优化:减少等待超时
|
||||
fastcgi_connect_timeout 60s;
|
||||
fastcgi_send_timeout 60s;
|
||||
fastcgi_read_timeout 60s;
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 4 128k;
|
||||
fastcgi_busy_buffers_size 256k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
}
|
||||
|
||||
# 首页通用路由
|
||||
location / {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
|
||||
# api目录转发
|
||||
location /api/ {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
|
||||
# 静态资源统一长期缓存(图片、样式、脚本、字体合并规则)
|
||||
location ~* \.(jpg|jpeg|png|gif|webp|ico|css|js|woff|woff2|ttf)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 禁止外部直接访问cache缓存目录,返回403
|
||||
location ^~ /cache/ {
|
||||
deny all;
|
||||
return 403;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm index.php;
|
||||
|
||||
# 统一PHP解析规则(所有目录下.php文件都交给php-fpm解析)
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
# 自动获取当前请求php文件真实路径,支持任意php文件
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
# 前端静态页面、目录访问
|
||||
location / {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
|
||||
# /api 仅路由,不用重复配置php,上面正则会自动处理
|
||||
location /api/ {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm index.php;
|
||||
|
||||
# 开启Gzip全局压缩(文本类压缩60%-80%)
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||
gzip_vary on;
|
||||
|
||||
# PHP统一解析 + 优化参数
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
|
||||
# PHP-FPM优化:减少等待超时
|
||||
fastcgi_connect_timeout 60s;
|
||||
fastcgi_send_timeout 60s;
|
||||
fastcgi_read_timeout 60s;
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 4 128k;
|
||||
fastcgi_busy_buffers_size 256k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
}
|
||||
|
||||
# 首页通用路由
|
||||
location / {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
|
||||
# api目录转发
|
||||
location /api/ {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
|
||||
# 静态资源专项缓存(头像、js、css、字体)
|
||||
location ~* \.(jpg|jpeg|png|gif|webp|ico)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
access_log off;
|
||||
}
|
||||
location ~* \.(js|css|woff|woff2|ttf)$ {
|
||||
expires 3d;
|
||||
add_header Cache-Control "public, max-age=259200";
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm index.php;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||
gzip_vary on;
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
fastcgi_connect_timeout 60s;
|
||||
fastcgi_send_timeout 60s;
|
||||
fastcgi_read_timeout 60s;
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 4 128k;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
|
||||
# 静态资源长期缓存
|
||||
location ~* \.(jpg|jpeg|png|gif|webp|ico)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
access_log off;
|
||||
}
|
||||
location ~* \.(js|css|woff|woff2|ttf)$ {
|
||||
expires 3d;
|
||||
add_header Cache-Control "public, max-age=259200";
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm index.php;
|
||||
|
||||
# 开启Gzip压缩,提升页面加载速度
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||
gzip_vary on;
|
||||
|
||||
# PHP解析配置
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
|
||||
# 优化PHP连接超时、缓冲区
|
||||
fastcgi_connect_timeout 60s;
|
||||
fastcgi_send_timeout 60s;
|
||||
fastcgi_read_timeout 60s;
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 4 128k;
|
||||
fastcgi_busy_buffers_size 256k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
}
|
||||
|
||||
# 首页通用路由
|
||||
location / {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
|
||||
# API目录
|
||||
location /api/ {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
|
||||
# 静态资源长期缓存(本地static目录js/css/图片)
|
||||
location ~* \.(jpg|jpeg|png|gif|webp|ico)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
access_log off;
|
||||
}
|
||||
location ~* \.(js|css|woff|woff2|ttf)$ {
|
||||
expires 3d;
|
||||
add_header Cache-Control "public, max-age=259200";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 禁止访问缓存目录cache,防止外部直接下载缓存文件
|
||||
location ^~ /cache/ {
|
||||
deny all;
|
||||
return 403;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm index.php;
|
||||
|
||||
# 前端静态页面
|
||||
location / {
|
||||
try_files $uri $uri/;
|
||||
}
|
||||
|
||||
# PHP告警接口
|
||||
location /api {
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html/api/index.php;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import poplib
|
||||
import ssl
|
||||
import email
|
||||
from email.header import decode_header
|
||||
import time
|
||||
import pymysql
|
||||
|
||||
# 关闭SSL校验,解决老Python握手重置
|
||||
#ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
# ========== 邮箱配置 自行修改 ==========
|
||||
MAIL_HOST = "pop.163.com"
|
||||
MAIL_PORT = 995
|
||||
MAIL_USER = "lyudream@163.com"
|
||||
MAIL_PWD = "DVZxyF5NKCkR6fL5"
|
||||
|
||||
# ========== 本地MariaDB配置 ==========
|
||||
DB_HOST = "127.0.0.1"
|
||||
DB_USER = "root"
|
||||
DB_PWD = "hp93000"
|
||||
DB_NAME = "alert_mail_stat"
|
||||
|
||||
# 解码邮件标题、正文
|
||||
def decode_str(s):
|
||||
value, charset = decode_header(s)[0]
|
||||
if charset:
|
||||
value = value.decode(charset)
|
||||
return value
|
||||
|
||||
# 获取邮件正文
|
||||
def get_email_content(msg):
|
||||
content = ""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
content_disposition = str(part.get("Content-Disposition"))
|
||||
if content_type == 'text/plain' and 'attachment' not in content_disposition:
|
||||
payload = part.get_payload(decode=True)
|
||||
charset = part.get_charset()
|
||||
if charset is None:
|
||||
charset = 'utf-8'
|
||||
content = payload.decode(charset, errors='ignore')
|
||||
break
|
||||
else:
|
||||
payload = msg.get_payload(decode=True)
|
||||
charset = msg.get_charset()
|
||||
if charset is None:
|
||||
charset = 'utf-8'
|
||||
content = payload.decode(charset, errors='ignore')
|
||||
return content
|
||||
|
||||
# 简单解析告警关键字(适配Alertmanager中文邮件模板:告警名称、实例、级别)
|
||||
def parse_alert_info(text):
|
||||
alert_name = ""
|
||||
instance = ""
|
||||
severity = "unknown"
|
||||
lines = text.splitlines()
|
||||
for line in lines:
|
||||
if "告警名称" in line:
|
||||
alert_name = line.split(":")[-1].strip()
|
||||
if "实例" in line:
|
||||
instance = line.split(":")[-1].strip()
|
||||
if "级别" in line:
|
||||
s_val = line.split(":")[-1].strip()
|
||||
if s_val.lower() in ["critical", "严重"]:
|
||||
severity = "critical"
|
||||
elif s_val.lower() in ["warning", "警告"]:
|
||||
severity = "warning"
|
||||
return alert_name, instance, severity
|
||||
|
||||
# 主抓取逻辑
|
||||
def crawl_mail():
|
||||
# 连接数据库
|
||||
db = pymysql.connect(host=DB_HOST, user=DB_USER, password=DB_PWD, database=DB_NAME, charset='utf8mb4')
|
||||
cursor = db.cursor()
|
||||
|
||||
# 连接POP3邮箱
|
||||
pop_conn = poplib.POP3_SSL(MAIL_HOST, MAIL_PORT)
|
||||
pop_conn.user(MAIL_USER)
|
||||
pop_conn.pass_(MAIL_PWD)
|
||||
total_num, total_size = pop_conn.stat()
|
||||
print(f"当前邮箱总邮件数量: {total_num}")
|
||||
|
||||
if total_num == 0:
|
||||
print("暂无新邮件")
|
||||
pop_conn.quit()
|
||||
db.close()
|
||||
return
|
||||
|
||||
# 从最新一封开始遍历
|
||||
for idx in range(total_num, 0, -1):
|
||||
# 获取邮件原始内容
|
||||
resp, raw_lines, octet = pop_conn.retr(idx)
|
||||
raw_msg = b'\r\n'.join(raw_lines).decode('utf-8', errors='ignore')
|
||||
msg = email.message_from_string(raw_msg)
|
||||
|
||||
# 生成唯一标识去重
|
||||
mail_uid = msg.get("Message-ID", str(time.time()))
|
||||
|
||||
# 查重,避免重复入库
|
||||
cursor.execute("SELECT id FROM alert_log WHERE mail_uid=%s", (mail_uid,))
|
||||
if cursor.fetchone():
|
||||
print(f"邮件{idx}已存在数据库,跳过")
|
||||
continue
|
||||
|
||||
# 解析标题、发件人
|
||||
subject = decode_str(msg.get("Subject", ""))
|
||||
from_addr = msg.get("From", "")
|
||||
|
||||
# ===================== 临时关闭发件人过滤,全部邮件放行测试 =====================
|
||||
# if "prometheusalert" not in from_addr.lower():
|
||||
# print(f"邮件{idx}发件人不匹配,跳过,发件人:{from_addr}")
|
||||
# continue
|
||||
|
||||
# 调试打印所有邮件基础信息
|
||||
print("\n========================================")
|
||||
print(f"【调试】正在处理第 {idx} 封邮件")
|
||||
print(f"发件人:{from_addr}")
|
||||
print(f"邮件标题:{subject}")
|
||||
print("========================================")
|
||||
|
||||
# 正文解析告警字段
|
||||
body = get_email_content(msg)
|
||||
alert_name, instance, severity = parse_alert_info(body)
|
||||
if not alert_name:
|
||||
print(f"邮件{idx}未识别到【告警名称】关键字,跳过")
|
||||
continue
|
||||
|
||||
# 区分触发/恢复告警
|
||||
alert_type = 1
|
||||
if "恢复" in subject or "resolved" in subject.lower():
|
||||
alert_type = 2
|
||||
|
||||
now = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
sql = """
|
||||
INSERT INTO alert_log
|
||||
(mail_uid, alert_type, alert_name, instance, severity, starts_at, ends_at, content, receive_time)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
cursor.execute(sql, (mail_uid, alert_type, alert_name, instance, severity, now, None, body, now))
|
||||
db.commit()
|
||||
print(f"入库成功 | 告警:{alert_name} 实例:{instance}")
|
||||
|
||||
pop_conn.quit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
print("\n本次抓取全部完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
crawl_mail()
|
||||
@@ -1,140 +0,0 @@
|
||||
import poplib
|
||||
import ssl
|
||||
import email
|
||||
from email.header import decode_header
|
||||
import time
|
||||
import pymysql
|
||||
|
||||
# 关闭SSL校验,解决老Python握手重置
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
# ========== 邮箱配置 自行修改 ==========
|
||||
MAIL_HOST = "pop.163.com"
|
||||
MAIL_PORT = 995
|
||||
MAIL_USER = "lyudream@163.com"
|
||||
MAIL_PWD = "DVZxyF5NKCkR6fL5"
|
||||
|
||||
# ========== 本地MariaDB配置 ==========
|
||||
DB_HOST = "127.0.0.1"
|
||||
DB_USER = "root"
|
||||
DB_PWD = "hp93000"
|
||||
DB_NAME = "alert_mail_stat"
|
||||
|
||||
# 解码邮件标题、正文
|
||||
def decode_str(s):
|
||||
value, charset = decode_header(s)[0]
|
||||
if charset:
|
||||
value = value.decode(charset)
|
||||
return value
|
||||
|
||||
# 获取邮件正文
|
||||
def get_email_content(msg):
|
||||
content = ""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
content_disposition = str(part.get("Content-Disposition"))
|
||||
if content_type == 'text/plain' and 'attachment' not in content_disposition:
|
||||
payload = part.get_payload(decode=True)
|
||||
charset = part.get_charset()
|
||||
if charset is None:
|
||||
charset = 'utf-8'
|
||||
content = payload.decode(charset, errors='ignore')
|
||||
break
|
||||
else:
|
||||
payload = msg.get_payload(decode=True)
|
||||
charset = msg.get_charset()
|
||||
if charset is None:
|
||||
charset = 'utf-8'
|
||||
content = payload.decode(charset, errors='ignore')
|
||||
return content
|
||||
|
||||
# 简单解析告警关键字(适配Alertmanager中文邮件模板:告警名称、实例、级别)
|
||||
def parse_alert_info(text):
|
||||
alert_name = ""
|
||||
instance = ""
|
||||
severity = "unknown"
|
||||
lines = text.splitlines()
|
||||
for line in lines:
|
||||
if "告警名称" in line:
|
||||
alert_name = line.split(":")[-1].strip()
|
||||
if "实例" in line:
|
||||
instance = line.split(":")[-1].strip()
|
||||
if "级别" in line:
|
||||
s_val = line.split(":")[-1].strip()
|
||||
if s_val.lower() in ["critical", "严重"]:
|
||||
severity = "critical"
|
||||
elif s_val.lower() in ["warning", "警告"]:
|
||||
severity = "warning"
|
||||
return alert_name, instance, severity
|
||||
|
||||
# 主抓取逻辑
|
||||
def crawl_mail():
|
||||
# 连接数据库
|
||||
db = pymysql.connect(host=DB_HOST, user=DB_USER, password=DB_PWD, database=DB_NAME, charset='utf8mb4')
|
||||
cursor = db.cursor()
|
||||
|
||||
# 连接POP3邮箱
|
||||
pop_conn = poplib.POP3_SSL(MAIL_HOST, MAIL_PORT)
|
||||
pop_conn.user(MAIL_USER)
|
||||
pop_conn.pass_(MAIL_PWD)
|
||||
total_num, total_size = pop_conn.stat()
|
||||
print(f"当前邮箱总邮件数量: {total_num}")
|
||||
|
||||
if total_num == 0:
|
||||
print("暂无新邮件")
|
||||
pop_conn.quit()
|
||||
db.close()
|
||||
return
|
||||
|
||||
# 从最新一封开始遍历
|
||||
for idx in range(total_num, 0, -1):
|
||||
# 获取邮件原始内容
|
||||
resp, raw_lines, octet = pop_conn.retr(idx)
|
||||
raw_msg = b'\r\n'.join(raw_lines).decode('utf-8', errors='ignore')
|
||||
msg = email.message_from_string(raw_msg)
|
||||
|
||||
# 生成唯一标识去重
|
||||
mail_uid = msg.get("Message-ID", str(time.time()))
|
||||
|
||||
# 查重,避免重复入库
|
||||
cursor.execute("SELECT id FROM alert_log WHERE mail_uid=%s", (mail_uid,))
|
||||
if cursor.fetchone():
|
||||
continue
|
||||
|
||||
# 解析标题、发件人
|
||||
subject = decode_str(msg.get("Subject", ""))
|
||||
from_addr = msg.get("From", "")
|
||||
# 过滤非告警发件人(自行修改匹配你的Alertmanager发件地址)
|
||||
if "prometheusalert" not in from_addr.lower():
|
||||
continue
|
||||
|
||||
# 正文解析告警字段
|
||||
body = get_email_content(msg)
|
||||
alert_name, instance, severity = parse_alert_info(body)
|
||||
if not alert_name:
|
||||
print(f"邮件{idx}未识别到告警,跳过")
|
||||
continue
|
||||
|
||||
# 区分触发/恢复告警
|
||||
alert_type = 1
|
||||
if "恢复" in subject or "resolved" in subject.lower():
|
||||
alert_type = 2
|
||||
|
||||
now = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
sql = """
|
||||
INSERT INTO alert_log
|
||||
(mail_uid, alert_type, alert_name, instance, severity, starts_at, ends_at, content, receive_time)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
cursor.execute(sql, (mail_uid, alert_type, alert_name, instance, severity, now, None, body, now))
|
||||
db.commit()
|
||||
print(f"入库成功 | 告警:{alert_name} 实例:{instance}")
|
||||
|
||||
pop_conn.quit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
print("本次抓取完成\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
crawl_mail()
|
||||
@@ -1,77 +0,0 @@
|
||||
import poplib
|
||||
import ssl
|
||||
import email
|
||||
from email.header import decode_header
|
||||
|
||||
# SSL兼容修复
|
||||
try:
|
||||
ctx = ssl._create_unverified_context
|
||||
except:
|
||||
ctx = None
|
||||
if ctx:
|
||||
ssl._create_default_https_context = ctx
|
||||
|
||||
# ========= 修改这里你的邮箱信息 =========
|
||||
MAIL_HOST = "pop.163.com"
|
||||
MAIL_PORT = 995
|
||||
MAIL_USER = "lyudream@163.com"
|
||||
MAIL_PWD = "DVZxyF5NKCkR6fL5"
|
||||
|
||||
def decode_text(s):
|
||||
value, charset = decode_header(s)[0]
|
||||
if charset:
|
||||
value = value.decode(charset)
|
||||
return value
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 开始连接pop.163.com:995 ===")
|
||||
try:
|
||||
pop = poplib.POP3_SSL(MAIL_HOST, MAIL_PORT)
|
||||
print("连接服务器成功,正在登录...")
|
||||
pop.user(MAIL_USER)
|
||||
pop.pass_(MAIL_PWD)
|
||||
print("登录邮箱成功!")
|
||||
|
||||
total, _ = pop.stat()
|
||||
print(f"邮箱共有邮件:{total} 封\n")
|
||||
if total == 0:
|
||||
print("邮箱无邮件")
|
||||
pop.quit()
|
||||
exit()
|
||||
|
||||
# 从最新邮件遍历
|
||||
for i in range(total, 0, -1):
|
||||
resp, lines, octet = pop.retr(i)
|
||||
msg_raw = b"\r\n".join(lines).decode("utf-8", errors="ignore")
|
||||
msg = email.message_from_string(msg_raw)
|
||||
|
||||
subject = decode_text(msg.get("Subject", ""))
|
||||
from_addr = msg.get("From", "")
|
||||
|
||||
# 取正文
|
||||
body = ""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
t = part.get_content_type()
|
||||
disp = str(part.get("Content-Disposition"))
|
||||
if t == "text/plain" and "attachment" not in disp:
|
||||
payload = part.get_payload(decode=True)
|
||||
body = payload.decode("utf-8", errors="ignore")
|
||||
break
|
||||
else:
|
||||
payload = msg.get_payload(decode=True)
|
||||
body = payload.decode("utf-8", errors="ignore")
|
||||
|
||||
print("========================================")
|
||||
print(f"第{i}封邮件")
|
||||
print(f"发件人:{from_addr}")
|
||||
print(f"标题:{subject}")
|
||||
print(f"正文内容:\n{body}")
|
||||
print("========================================\n")
|
||||
|
||||
pop.quit()
|
||||
print("全部读取完成")
|
||||
|
||||
except Exception as e:
|
||||
print("连接/登录失败,错误信息:")
|
||||
print(str(e))
|
||||
@@ -1,205 +0,0 @@
|
||||
nohup: ignoring input
|
||||
* Serving Flask app "webhook" (lazy loading)
|
||||
* Environment: production
|
||||
WARNING: This is a development server. Do not use it in a production deployment.
|
||||
Use a production WSGI server instead.
|
||||
* Debug mode: off
|
||||
* Running on http://0.0.0.0:909/ (Press CTRL+C to quit)
|
||||
10.150.117.190 - - [02/Jul/2026 14:34:17] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:34:17] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:34:17] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:34:17] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:35:51] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:35:51] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:35:51] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:35:51] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:42:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:42:41] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:42:41] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:42:41] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:42:41] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:42:42] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:42:42] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:42:48] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:41] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:41] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:41] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:41] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:41] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:41] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:43:56] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:11] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:11] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:11] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:44:18] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:45:06] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:45:06] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:45:06] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:45:06] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:45:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:45:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:45:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:56:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:56:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:56:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:56:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:56:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:56:46] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:58:57] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:59:12] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:59:12] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 14:59:12] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 17:28:04] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 19:23:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [02/Jul/2026 19:24:03] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [03/Jul/2026 12:30:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [03/Jul/2026 12:30:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [03/Jul/2026 15:19:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [03/Jul/2026 15:19:53] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [04/Jul/2026 13:44:13] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [04/Jul/2026 13:50:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [04/Jul/2026 13:56:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [04/Jul/2026 13:57:03] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [04/Jul/2026 14:26:13] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [04/Jul/2026 14:28:53] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [04/Jul/2026 14:33:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [04/Jul/2026 14:33:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.44.16 - - [06/Jul/2026 09:36:01] "[33mGET / HTTP/1.1[0m" 404 -
|
||||
10.150.44.16 - - [06/Jul/2026 09:36:02] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
||||
10.150.117.190 - - [06/Jul/2026 11:21:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 11:21:31] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 11:25:06] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 11:28:03] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 11:28:51] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 11:30:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 11:30:31] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 11:31:46] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 12:21:51] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 13:54:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 13:55:18] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:44:18] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:45:06] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:45:26] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:45:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:50:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:50:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:52:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:52:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:54:13] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:54:13] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:54:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 14:54:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 15:13:13] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 15:13:33] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 15:16:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 15:16:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 15:18:48] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 15:18:48] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 15:49:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 15:50:48] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 16:00:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 16:01:08] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [06/Jul/2026 17:22:05] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 09:42:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 09:43:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 09:46:13] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 09:46:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 10:11:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 10:11:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 10:13:08] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 10:13:08] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 10:23:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 10:24:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 10:25:18] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 10:29:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 10:50:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 11:21:01] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 11:24:13] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 11:24:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 11:37:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 11:37:31] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 11:43:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 11:44:06] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 11:44:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 11:45:03] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 11:46:03] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 12:12:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 12:13:48] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
@@ -1,65 +0,0 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import pymysql
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def get_db():
|
||||
return pymysql.connect(
|
||||
host="127.0.0.1",
|
||||
user="root",
|
||||
password="hp93000",
|
||||
database="alert_mail_stat",
|
||||
charset="utf8mb4"
|
||||
)
|
||||
|
||||
@app.route("/alert", methods=["POST"])
|
||||
def alert_receive():
|
||||
try:
|
||||
data = request.get_json()
|
||||
alerts = data.get("alerts", [])
|
||||
insert_num = 0
|
||||
update_num = 0
|
||||
db = get_db()
|
||||
cur = db.cursor()
|
||||
|
||||
for alert in alerts:
|
||||
fp = alert["fingerprint"]
|
||||
alert_name = alert["labels"].get("alertname", "")
|
||||
instance = alert["labels"].get("instance", "")
|
||||
severity = alert["labels"].get("severity", "warning")
|
||||
status = alert["status"]
|
||||
start_at = alert.get("startsAt", "")
|
||||
end_at = alert.get("endsAt")
|
||||
alert_type = 2 if status == "resolved" else 1
|
||||
content_str = str(alert)
|
||||
|
||||
# 1. 原有告警入库逻辑不变
|
||||
sql = """
|
||||
INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
|
||||
VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
|
||||
"""
|
||||
cur.execute(sql, (fp, alert_type, alert_name, instance, severity, start_at, end_at, content_str))
|
||||
insert_num += 1
|
||||
|
||||
# 2. 新增:同步监控规则,存在则置为启用1
|
||||
if alert_name:
|
||||
sync_rule_sql = """
|
||||
INSERT INTO monitor_rule (rule_name, status)
|
||||
VALUES (%s, 1)
|
||||
ON DUPLICATE KEY UPDATE status = 1
|
||||
"""
|
||||
cur.execute(sync_rule_sql, [alert_name])
|
||||
|
||||
db.commit()
|
||||
cur.close()
|
||||
db.close()
|
||||
return jsonify({"code": 200, "insert": insert_num, "update": update_num})
|
||||
|
||||
except Exception as e:
|
||||
if "db" in locals():
|
||||
db.rollback()
|
||||
db.close()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 200
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=909, debug=False)
|
||||
@@ -1,47 +0,0 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import pymysql
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def get_db():
|
||||
return pymysql.connect(
|
||||
host="127.0.0.1",
|
||||
user="root",
|
||||
password="hp93000",
|
||||
database="alert_mail_stat",
|
||||
charset="utf8mb4"
|
||||
)
|
||||
|
||||
@app.route("/alert", methods=["POST"])
|
||||
def alert_in():
|
||||
raw = request.get_json()
|
||||
alerts = raw.get("alerts", [])
|
||||
add_cnt = 0
|
||||
db = get_db()
|
||||
cur = db.cursor()
|
||||
for alert in alerts:
|
||||
fp = alert["fingerprint"]
|
||||
alert_name = alert["labels"].get("alertname", "")
|
||||
instance = alert["labels"].get("instance", "")
|
||||
severity = alert["labels"].get("severity", "warning")
|
||||
status = alert["status"]
|
||||
start = alert.get("startsAt", "")
|
||||
end = alert.get("endsAt")
|
||||
type_val = 2 if status == "resolved" else 1
|
||||
|
||||
cur.execute("SELECT id FROM alert_log WHERE mail_uid=%s", (fp,))
|
||||
if cur.fetchone():
|
||||
continue
|
||||
sql = """
|
||||
INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
|
||||
VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
|
||||
"""
|
||||
cur.execute(sql, (fp, type_val, alert_name, instance, severity, start, end, str(alert)))
|
||||
add_cnt += 1
|
||||
db.commit()
|
||||
cur.close()
|
||||
db.close()
|
||||
return jsonify({"code": 200, "insert_count": add_cnt})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=909, debug=False)
|
||||
@@ -1,47 +0,0 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import pymysql
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def get_db():
|
||||
return pymysql.connect(
|
||||
host="127.0.0.1",
|
||||
user="root",
|
||||
password="hp93000",
|
||||
database="alert_mail_stat",
|
||||
charset="utf8mb4"
|
||||
)
|
||||
|
||||
@app.route("/alert", methods=["POST"])
|
||||
def alert_receive():
|
||||
data = request.get_json()
|
||||
alerts = data.get("alerts", [])
|
||||
insert_num = 0
|
||||
db = get_db()
|
||||
cur = db.cursor()
|
||||
for alert in alerts:
|
||||
fp = alert["fingerprint"]
|
||||
alert_name = alert["labels"].get("alertname", "")
|
||||
instance = alert["labels"].get("instance", "")
|
||||
severity = alert["labels"].get("severity", "warning")
|
||||
status = alert["status"]
|
||||
start_at = alert.get("startsAt", "")
|
||||
end_at = alert.get("endsAt")
|
||||
alert_type = 2 if status == "resolved" else 1
|
||||
|
||||
cur.execute("SELECT id FROM alert_log WHERE mail_uid=%s", (fp,))
|
||||
if cur.fetchone():
|
||||
continue
|
||||
sql = """
|
||||
INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
|
||||
VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
|
||||
"""
|
||||
cur.execute(sql, (fp, alert_type, alert_name, instance, severity, start_at, end_at, str(alert)))
|
||||
insert_num += 1
|
||||
db.commit()
|
||||
cur.close()
|
||||
db.close()
|
||||
return jsonify({"code": 200, "insert": insert_num})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=909, debug=False)
|
||||
@@ -1,72 +0,0 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import pymysql
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def get_db():
|
||||
return pymysql.connect(
|
||||
host="127.0.0.1",
|
||||
user="root",
|
||||
password="hp93000",
|
||||
database="alert_mail_stat",
|
||||
charset="utf8mb4"
|
||||
)
|
||||
|
||||
@app.route("/alert", methods=["POST"])
|
||||
def alert_receive():
|
||||
try:
|
||||
data = request.get_json()
|
||||
alerts = data.get("alerts", [])
|
||||
insert_num = 0
|
||||
update_num = 0
|
||||
db = get_db()
|
||||
cur = db.cursor()
|
||||
|
||||
for alert in alerts:
|
||||
fp = alert["fingerprint"]
|
||||
alert_name = alert["labels"].get("alertname", "")
|
||||
instance = alert["labels"].get("instance", "")
|
||||
severity = alert["labels"].get("severity", "warning")
|
||||
status = alert["status"]
|
||||
start_at = alert.get("startsAt", "")
|
||||
end_at = alert.get("endsAt")
|
||||
alert_type = 2 if status == "resolved" else 1
|
||||
content_str = str(alert)
|
||||
|
||||
cur.execute("""
|
||||
SELECT id FROM alert_log
|
||||
WHERE mail_uid=%s AND DATE(receive_time) = CURDATE()
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""", (fp,))
|
||||
row = cur.fetchone()
|
||||
|
||||
if status == "resolved":
|
||||
if row:
|
||||
cur.execute("""
|
||||
UPDATE alert_log
|
||||
SET alert_type=%s, ends_at=%s, content=%s
|
||||
WHERE mail_uid=%s AND id=%s
|
||||
""", (alert_type, end_at, content_str, fp, row[0]))
|
||||
update_num += 1
|
||||
else:
|
||||
# 删掉唯一索引后,每次故障直接插入,不再判断是否存在
|
||||
sql = """
|
||||
INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
|
||||
VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
|
||||
"""
|
||||
cur.execute(sql, (fp, alert_type, alert_name, instance, severity, start_at, end_at, content_str))
|
||||
insert_num += 1
|
||||
|
||||
db.commit()
|
||||
cur.close()
|
||||
db.close()
|
||||
return jsonify({"code": 200, "insert": insert_num, "update": update_num})
|
||||
|
||||
except Exception as e:
|
||||
if "db" in locals():
|
||||
db.rollback()
|
||||
db.close()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 200
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=909, debug=False)
|
||||
@@ -1,55 +0,0 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import pymysql
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def get_db():
|
||||
return pymysql.connect(
|
||||
host="127.0.0.1",
|
||||
user="root",
|
||||
password="hp93000",
|
||||
database="alert_mail_stat",
|
||||
charset="utf8mb4"
|
||||
)
|
||||
|
||||
@app.route("/alert", methods=["POST"])
|
||||
def alert_receive():
|
||||
try:
|
||||
data = request.get_json()
|
||||
alerts = data.get("alerts", [])
|
||||
insert_num = 0
|
||||
update_num = 0
|
||||
db = get_db()
|
||||
cur = db.cursor()
|
||||
|
||||
for alert in alerts:
|
||||
fp = alert["fingerprint"]
|
||||
alert_name = alert["labels"].get("alertname", "")
|
||||
instance = alert["labels"].get("instance", "")
|
||||
severity = alert["labels"].get("severity", "warning")
|
||||
status = alert["status"]
|
||||
start_at = alert.get("startsAt", "")
|
||||
end_at = alert.get("endsAt")
|
||||
alert_type = 2 if status == "resolved" else 1
|
||||
content_str = str(alert)
|
||||
|
||||
sql = """
|
||||
INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
|
||||
VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
|
||||
"""
|
||||
cur.execute(sql, (fp, alert_type, alert_name, instance, severity, start_at, end_at, content_str))
|
||||
insert_num += 1
|
||||
|
||||
db.commit()
|
||||
cur.close()
|
||||
db.close()
|
||||
return jsonify({"code": 200, "insert": insert_num, "update": update_num})
|
||||
|
||||
except Exception as e:
|
||||
if "db" in locals():
|
||||
db.rollback()
|
||||
db.close()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 200
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=909, debug=False)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@
|
||||
session_start();
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
require_once 'auth.php';
|
||||
$expire = 1800;
|
||||
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
|
||||
session_destroy();
|
||||
@@ -80,6 +81,7 @@ td{padding:12px 10px;border-bottom:1px solid #F2F3F5;color:#1D2129;}
|
||||
<div class="nav-right">
|
||||
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a>
|
||||
<span style="color:#4E5969;"><i class="fa fa-user"></i> <?php echo $userName; ?></span>
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleRealName; ?>)</span>
|
||||
<button class="btn btn-danger" id="logoutBtn"><i class="fa fa-sign-out"></i> 退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
+165
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
session_start();
|
||||
header("Access-Control-Allow-Credentials: true");
|
||||
header("Content-Type:application/json;charset=utf-8");
|
||||
|
||||
$SECRET_RAW = "DockerAdminPlatform2026SecretKey123456789";
|
||||
|
||||
$uidGet = $_GET['uid'] ?? '';
|
||||
if (!empty($uidGet) && empty($_SESSION['user_id'])) {
|
||||
$_SESSION['user_id'] = $uidGet;
|
||||
$_SESSION['login_time'] = time();
|
||||
}
|
||||
|
||||
$debug = [
|
||||
"sid" => session_id(),
|
||||
"session_data" => $_SESSION
|
||||
];
|
||||
|
||||
$expire = 1800;
|
||||
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
|
||||
session_destroy();
|
||||
echo json_encode([
|
||||
"code"=>401,
|
||||
"msg"=>"未登录",
|
||||
"debug"=>$debug
|
||||
],JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = $_SESSION['user_id'];
|
||||
$role = $_SESSION['role'];
|
||||
$ts = time();
|
||||
$rawSignStr = $user . "|" . $role . "|" . $ts;
|
||||
$sign = hash_hmac("sha256", $rawSignStr, $SECRET_RAW);
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'];
|
||||
|
||||
$act = $_POST['act'] ?? $_GET['act'] ?? 'list';
|
||||
$cid = $_POST['container_id'] ?? $_GET['container_id'] ?? '';
|
||||
$imageId = $_POST['image_id'] ?? $_GET['image_id'] ?? '';
|
||||
$lines = $_POST['lines'] ?? $_GET['lines'] ?? 200;
|
||||
$postRaw = file_get_contents("php://input");
|
||||
$apiBase = "http://10.150.117.190:8090";
|
||||
|
||||
$authHeaders = [
|
||||
"X-User: {$user}",
|
||||
"X-Role: {$role}",
|
||||
"X-Time: {$ts}",
|
||||
"X-Sign: {$sign}",
|
||||
"X-Client-Ip: {$clientIp}",
|
||||
"Content-Type: application/json; charset=utf-8"
|
||||
];
|
||||
$headerStr = implode("\r\n", $authHeaders);
|
||||
|
||||
$getMap = [
|
||||
'list' => '/list',
|
||||
'image_list' => '/image_list',
|
||||
'restart' => '/restart/%s',
|
||||
'stop' => '/stop/%s',
|
||||
'rm' => '/rm/%s',
|
||||
'remove_image' => '/remove_image/%s',
|
||||
'logs' => '/logs/%s/%d',
|
||||
'network' => '/network/%s',
|
||||
'stats' => '/stats/%s',
|
||||
'detail' => '/detail/%s'
|
||||
];
|
||||
|
||||
$postMap = [
|
||||
'exec' => '/exec/%s',
|
||||
'create' => '/create',
|
||||
'bind_repo' => '/bind_repo',
|
||||
];
|
||||
|
||||
if (isset($getMap[$act])) {
|
||||
$path = $getMap[$act];
|
||||
$url = '';
|
||||
switch ($act) {
|
||||
case 'restart':
|
||||
case 'stop':
|
||||
case 'rm':
|
||||
case 'network':
|
||||
case 'stats':
|
||||
case 'detail':
|
||||
if (empty($cid)) {
|
||||
echo json_encode(["code"=>400,"msg"=>"缺少容器ID","debug"=>$debug],JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
$url = $apiBase . sprintf($path, urlencode($cid));
|
||||
break;
|
||||
case 'remove_image':
|
||||
if (empty($imageId)) {
|
||||
echo json_encode(["code"=>400,"msg"=>"缺少镜像ID","debug"=>$debug],JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
$url = $apiBase . sprintf($path, urlencode($imageId));
|
||||
break;
|
||||
case 'logs':
|
||||
if (empty($cid)) {
|
||||
echo json_encode(["code"=>400,"msg"=>"缺少容器ID","debug"=>$debug],JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
$url = $apiBase . sprintf($path, urlencode($cid), (int)$lines);
|
||||
break;
|
||||
default:
|
||||
$url = $apiBase . $path;
|
||||
break;
|
||||
}
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => $headerStr,
|
||||
'timeout' => 30
|
||||
]
|
||||
]);
|
||||
$resp = @file_get_contents($url, false, $ctx);
|
||||
if ($resp === false) {
|
||||
echo json_encode([
|
||||
"code" => 503,
|
||||
"msg" => "后端Docker接口连接失败,请检查服务是否启动",
|
||||
"debug" => $debug
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
echo $resp;
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($postMap[$act])) {
|
||||
$path = $postMap[$act];
|
||||
$url = '';
|
||||
if ($act === 'exec') {
|
||||
if (empty($cid)) {
|
||||
echo json_encode(["code"=>400,"msg"=>"缺少容器ID","debug"=>$debug],JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
$url = $apiBase . sprintf($path, urlencode($cid));
|
||||
} else {
|
||||
$url = $apiBase . $path;
|
||||
}
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => $headerStr,
|
||||
'content' => $postRaw,
|
||||
'timeout' => 30
|
||||
]
|
||||
]);
|
||||
$resp = @file_get_contents($url, false, $ctx);
|
||||
if ($resp === false) {
|
||||
echo json_encode([
|
||||
"code" => 503,
|
||||
"msg" => "后端Docker接口连接失败,请检查服务是否启动",
|
||||
"debug" => $debug
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
echo $resp;
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
"code"=>400,
|
||||
"msg"=>"无效操作act参数",
|
||||
"debug"=>$debug
|
||||
],JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
@@ -0,0 +1,752 @@
|
||||
import json
|
||||
import shlex
|
||||
import time
|
||||
import hashlib
|
||||
import hmac
|
||||
import docker
|
||||
from flask import Flask, jsonify, request
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
# 全局签名密钥,前后端统一,生产环境使用环境变量注入
|
||||
SIGN_SECRET = b"DockerAdminPlatform2026SecretKey123456789"
|
||||
# 签名有效期 300秒(5分钟)
|
||||
SIGN_EXPIRE = 300
|
||||
|
||||
# 兼容新旧docker-py客户端
|
||||
try:
|
||||
cli = docker.DockerClient(base_url='unix:///var/run/docker.sock')
|
||||
api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock')
|
||||
except AttributeError:
|
||||
cli = docker.Client(base_url='unix:///var/run/docker.sock')
|
||||
api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock')
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'docker-terminal-secret-2026'
|
||||
|
||||
# WebSocket心跳保活,防止Nginx/反向代理断连
|
||||
socketio = SocketIO(
|
||||
app,
|
||||
cors_allowed_origins="*",
|
||||
async_mode="threading",
|
||||
ping_timeout=60,
|
||||
ping_interval=25
|
||||
)
|
||||
|
||||
# 全局会话存储
|
||||
session_exec_map = {} # sid => TTY终端流信息
|
||||
log_task_map = {} # sid => {"cid": "", "running": True}
|
||||
ws_user_map = {} # 新增:sid 映射 WebSocket鉴权用户信息
|
||||
registry_config = {
|
||||
"repo_name": "",
|
||||
"repo_addr": "",
|
||||
"repo_user": "",
|
||||
"repo_pwd": ""
|
||||
}
|
||||
# 多系统shell兼容列表(Ubuntu/Alpine/标准Linux)
|
||||
SHELL_CANDIDATES = [
|
||||
"/bin/bash",
|
||||
"/usr/bin/bash",
|
||||
"/bin/sh",
|
||||
"/bin/ash"
|
||||
]
|
||||
|
||||
# ===================== 工具函数1:docker socket兼容recv =====================
|
||||
def docker_recv(sock, size=1024):
|
||||
try:
|
||||
return sock.recv(size)
|
||||
except Exception:
|
||||
try:
|
||||
return sock._sock.recv(size)
|
||||
except Exception:
|
||||
return b""
|
||||
|
||||
# ===================== 工具函数2:操作审计日志(终端输入脱敏,不存明文密码) =====================
|
||||
def write_audit_log(user, role, action, target_name, target_id, result, client_ip):
|
||||
"""企业审计记录,可对接数据库docker_audit_log表"""
|
||||
# 终端输入仅标记事件,不记录明文命令,规避密码泄露风险
|
||||
if action == "terminal_input":
|
||||
result = "command_entered"
|
||||
log_item = {
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"user": user,
|
||||
"role": role,
|
||||
"action": action,
|
||||
"container_name": target_name,
|
||||
"container_id": target_id,
|
||||
"result": result,
|
||||
"client_ip": client_ip
|
||||
}
|
||||
# 生产环境替换为mysql insert语句
|
||||
print(json.dumps(log_item, ensure_ascii=False))
|
||||
|
||||
# ===================== 工具函数3:HTTP Header HMAC签名鉴权(原有,HTTP接口专用) =====================
|
||||
def verify_auth_sign():
|
||||
"""
|
||||
校验前端PHP生成的HMAC签名,防止伪造X-Role:admin
|
||||
请求Header要求:
|
||||
X-User: 用户名
|
||||
X-Role: admin/user/guest
|
||||
X-Time: 当前时间戳(秒)
|
||||
X-Sign: hmac-sha256签名
|
||||
返回 (user, role, client_ip, True/False)
|
||||
"""
|
||||
user = request.headers.get("X-User", "anonymous")
|
||||
role = request.headers.get("X-Role", "guest")
|
||||
ts_str = request.headers.get("X-Time", "0")
|
||||
sign = request.headers.get("X-Sign", "")
|
||||
client_ip = request.headers.get("X-Client-Ip", request.remote_addr)
|
||||
try:
|
||||
ts = int(ts_str)
|
||||
except ValueError:
|
||||
return user, role, client_ip, False
|
||||
# 校验签名有效期
|
||||
now = int(time.time())
|
||||
if abs(now - ts) > SIGN_EXPIRE:
|
||||
return user, role, client_ip, False
|
||||
# 校验HMAC签名
|
||||
raw_data = f"{user}|{role}|{ts}".encode("utf-8")
|
||||
calc_sign = hmac.new(SIGN_SECRET, raw_data, hashlib.sha256).hexdigest()
|
||||
if calc_sign != sign:
|
||||
return user, role, client_ip, False
|
||||
return user, role, client_ip, True
|
||||
|
||||
# ===================== 新增工具函数4:WebSocket连接专用鉴权函数 =====================
|
||||
def verify_socket_auth(auth):
|
||||
user = auth.get("user", "")
|
||||
role = auth.get("role", "")
|
||||
ts_str = auth.get("ts", "0")
|
||||
sign = auth.get("sign", "")
|
||||
try:
|
||||
ts = int(ts_str)
|
||||
except:
|
||||
return user, role, "", False
|
||||
if abs(int(time.time()) - ts) > SIGN_EXPIRE:
|
||||
return user, role, "", False
|
||||
raw_data = f"{user}|{role}|{ts}".encode("utf-8")
|
||||
calc_sign = hmac.new(
|
||||
SIGN_SECRET,
|
||||
raw_data,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
if calc_sign != sign:
|
||||
return user, role, "", False
|
||||
return user, role, "", True
|
||||
|
||||
# ===================== 工具函数5:环境变量脱敏,屏蔽密码/密钥 =====================
|
||||
def safe_env_filter(env_list):
|
||||
safe_env = []
|
||||
sensitive_keywords = ["PASS", "PASSWORD", "TOKEN", "SECRET", "KEY", "CREDENTIAL"]
|
||||
for item in env_list:
|
||||
equal_pos = item.find("=")
|
||||
if equal_pos == -1:
|
||||
safe_env.append(item)
|
||||
continue
|
||||
key = item[:equal_pos]
|
||||
val = item[equal_pos+1:]
|
||||
if any(word in key.upper() for word in sensitive_keywords):
|
||||
safe_env.append(f"{key}=******")
|
||||
else:
|
||||
safe_env.append(item)
|
||||
return safe_env
|
||||
|
||||
# ===================== 全部HTTP业务接口 =====================
|
||||
# 1. 容器列表(端口预拼接字符串,前端直接渲染,无需formatPorts)
|
||||
@app.route("/list")
|
||||
def api_list():
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "list_container", "", "", "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
arr = []
|
||||
for c in cli.containers.list(all=True):
|
||||
img_name = c.image.tags[0] if c.image.tags else ""
|
||||
port_str_arr = []
|
||||
ports_raw = c.ports
|
||||
for c_port, host_list in ports_raw.items():
|
||||
if not host_list:
|
||||
continue
|
||||
for host_item in host_list:
|
||||
host_port = host_item.get("HostPort", "")
|
||||
port_str_arr.append(f"{host_port}:{c_port.split('/')[0]}")
|
||||
port_display = ",".join(port_str_arr) if port_str_arr else "-"
|
||||
arr.append({
|
||||
"id": c.short_id,
|
||||
"full_id": c.id,
|
||||
"name": c.name,
|
||||
"image": img_name,
|
||||
"status": c.status,
|
||||
"ports": port_display
|
||||
})
|
||||
write_audit_log(user, role, "list_container", "", "", "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","list":arr})
|
||||
|
||||
# 2. 镜像列表
|
||||
@app.route("/image_list")
|
||||
def api_image_list():
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "list_image", "", "", "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
arr = []
|
||||
for img in cli.images.list():
|
||||
if not img.tags:
|
||||
continue
|
||||
full_tag = img.tags[0]
|
||||
repo, tag = full_tag.split(":", 1)
|
||||
size_mb = round(img.attrs["Size"] / 1024 / 1024, 1)
|
||||
arr.append({
|
||||
"repo": repo,
|
||||
"tag": tag,
|
||||
"id": img.short_id,
|
||||
"full_id": img.id,
|
||||
"size": f"{size_mb}MB",
|
||||
"created": img.attrs["Created"]
|
||||
})
|
||||
write_audit_log(user, role, "list_image", "", "", "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","list":arr})
|
||||
|
||||
# 3. 删除镜像
|
||||
@app.route("/remove_image/<image_id>")
|
||||
def api_remove_image(image_id):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "remove_image", "", image_id, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "remove_image", "", image_id, "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可删除镜像"})
|
||||
try:
|
||||
img = cli.images.get(image_id)
|
||||
cli.images.remove(image_id, force=True)
|
||||
write_audit_log(user, role, "remove_image", img.tags[0] if img.tags else "", image_id, "success", ip)
|
||||
return jsonify({"code":0,"msg":"镜像已强制删除"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "remove_image", "", image_id, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 4. 重启容器
|
||||
@app.route("/restart/<cid>")
|
||||
def api_restart(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "restart", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "restart", "", cid, "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
container.restart()
|
||||
write_audit_log(user, role, "restart", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"容器已重启"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "restart", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 5. 停止容器
|
||||
@app.route("/stop/<cid>")
|
||||
def api_stop(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "stop", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "stop", "", cid, "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
container.stop()
|
||||
write_audit_log(user, role, "stop", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"容器已停止"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "stop", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 6. 删除容器
|
||||
@app.route("/rm/<cid>")
|
||||
def api_rm(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "rm", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "rm", "", cid, "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可删除容器"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
container.remove(force=True)
|
||||
write_audit_log(user, role, "rm", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"容器已强制删除"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "rm", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 7. 一次性拉取日志(兼容旧页面单次查看)
|
||||
@app.route("/logs/<cid>/<int:tail_lines>")
|
||||
def api_logs(cid, tail_lines):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "read_log", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
log_bytes = container.logs(tail=tail_lines)
|
||||
log_str = log_bytes.decode("utf-8", errors="replace")
|
||||
write_audit_log(user, role, "read_log", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","data":log_str})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "read_log", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e),"data":""})
|
||||
|
||||
# 8. 容器网络详情
|
||||
@app.route("/network/<cid>")
|
||||
def api_network(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "read_network", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
info = container.attrs["NetworkSettings"]["Networks"]
|
||||
write_audit_log(user, role, "read_network", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","data":json.dumps(info,indent=2)})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "read_network", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e),"data":""})
|
||||
|
||||
# 9. 短连接单次执行命令(旧弹窗兼容,非实时终端)
|
||||
@app.route("/exec/<cid>", methods=["POST"])
|
||||
def api_exec(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "single_exec", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "single_exec", "", cid, "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可执行命令"})
|
||||
try:
|
||||
data = request.get_json()
|
||||
cmd = data.get("cmd", "")
|
||||
if not cmd:
|
||||
return jsonify({"code":400,"msg":"命令不能为空","data":""})
|
||||
cmd_list = shlex.split(cmd)
|
||||
container = cli.containers.get(cid)
|
||||
exec_obj = container.exec_run(cmd_list, stdout=True, stderr=True)
|
||||
output = exec_obj.output.decode("utf-8", errors="replace")
|
||||
write_audit_log(user, role, "single_exec", container.name, cid, "command_executed", ip)
|
||||
return jsonify({"code":0,"msg":"success","data":output})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "single_exec", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e),"data":""})
|
||||
|
||||
# 10. 创建容器(端口标准格式 + 前端可配置CPU/内存)
|
||||
@app.route("/create", methods=["POST"])
|
||||
def api_create():
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "create_container", "", "", "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "create_container", "", "", "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可创建容器"})
|
||||
try:
|
||||
data = request.get_json()
|
||||
name = data.get("name", "")
|
||||
image = data.get("image")
|
||||
cmd = data.get("cmd")
|
||||
ports_input = data.get("ports", {})
|
||||
# 修复:docker标准端口字典格式 {内部端口:宿主机端口}
|
||||
port_bind = {}
|
||||
for inner_port, host_port in ports_input.items():
|
||||
port_bind[f"{inner_port}/tcp"] = int(host_port)
|
||||
# 前端可配置资源限制,默认1核512M
|
||||
cpu_core = int(data.get("cpu", 1))
|
||||
memory_limit = data.get("memory", "512m")
|
||||
nano_cpus = cpu_core * 1000000000
|
||||
container = cli.containers.run(
|
||||
image=image,
|
||||
name=name if name else None,
|
||||
command=cmd if cmd else None,
|
||||
ports=port_bind,
|
||||
detach=True,
|
||||
restart_policy={"Name": "always"},
|
||||
mem_limit=memory_limit,
|
||||
nano_cpus=nano_cpus
|
||||
)
|
||||
write_audit_log(user, role, "create_container", container.name, container.id, "success", ip)
|
||||
return jsonify({"code":0,"msg":f"容器创建成功,ID:{container.short_id}"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "create_container", "", "", f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 11. 登录私有镜像仓库
|
||||
@app.route("/bind_repo", methods=["POST"])
|
||||
def api_bind_repo():
|
||||
global registry_config
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "repo_login", "", "", "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "repo_login", "", "", "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可登录镜像仓库"})
|
||||
try:
|
||||
data = request.get_json()
|
||||
repo_name = data.get("repo_name","")
|
||||
repo_addr = data.get("repo_addr")
|
||||
repo_user = data.get("repo_user","")
|
||||
repo_pwd = data.get("repo_pwd","")
|
||||
api_cli.login(username=repo_user, password=repo_pwd, registry=repo_addr)
|
||||
registry_config["repo_name"] = repo_name
|
||||
registry_config["repo_addr"] = repo_addr
|
||||
registry_config["repo_user"] = repo_user
|
||||
registry_config["repo_pwd"] = repo_pwd
|
||||
write_audit_log(user, role, "repo_login", repo_addr, "", "success", ip)
|
||||
return jsonify({"code":0,"msg":"镜像仓库登录绑定成功"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "repo_login", repo_addr, "", f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 修复:stats接口online_cpus兜底兼容,无KeyError
|
||||
@app.route("/stats/<cid>")
|
||||
def api_stats(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "read_stats", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
raw = container.stats(stream=False)
|
||||
# CPU兼容兜底,不存在online_cpus自动计算
|
||||
cpu_stats = raw["cpu_stats"]
|
||||
online_cpus = cpu_stats.get("online_cpus", len(cpu_stats["cpu_usage"].get("percpu_usage", [])) or 1)
|
||||
cpu_delta = cpu_stats["cpu_usage"]["total_usage"] - raw["precpu_stats"]["cpu_usage"]["total_usage"]
|
||||
system_delta = cpu_stats["system_cpu_usage"] - raw["precpu_stats"]["system_cpu_usage"]
|
||||
cpu_percent = 0.0
|
||||
if system_delta > 0 and cpu_delta > 0:
|
||||
cpu_percent = round((cpu_delta / system_delta) * online_cpus * 100, 1)
|
||||
# 内存计算
|
||||
mem_usage = raw["memory_stats"]["usage"] / 1024 / 1024
|
||||
mem_limit = raw["memory_stats"]["limit"] / 1024 / 1024
|
||||
mem_percent = round((mem_usage / mem_limit) * 100, 1) if mem_limit > 0 else 0
|
||||
# 网络流量
|
||||
net_rx = 0
|
||||
net_tx = 0
|
||||
for net in raw["networks"].values():
|
||||
net_rx += net["rx_bytes"] / 1024 / 1024
|
||||
net_tx += net["tx_bytes"] / 1024 / 1024
|
||||
res = {
|
||||
"cpu": cpu_percent,
|
||||
"mem_mb": round(mem_usage, 1),
|
||||
"mem_limit_mb": round(mem_limit, 1),
|
||||
"mem_percent": mem_percent,
|
||||
"net_rx_mb": round(net_rx, 1),
|
||||
"net_tx_mb": round(net_tx, 1)
|
||||
}
|
||||
write_audit_log(user, role, "read_stats", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","data":res})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "read_stats", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e),"data":{}})
|
||||
|
||||
# 修复:detail接口ENV脱敏,多网卡IP兼容
|
||||
@app.route("/detail/<cid>")
|
||||
def api_detail(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "read_detail", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
attrs = container.attrs
|
||||
nets = attrs["NetworkSettings"]["Networks"]
|
||||
ip_map = {}
|
||||
for net_name, net_info in nets.items():
|
||||
ip_map[net_name] = net_info.get("IPAddress", "")
|
||||
# 敏感环境变量脱敏
|
||||
raw_env = attrs["Config"].get("Env", [])
|
||||
safe_env = safe_env_filter(raw_env)
|
||||
res = {
|
||||
"network_ips": ip_map,
|
||||
"mounts": attrs["Mounts"],
|
||||
"env": safe_env,
|
||||
"created": attrs["Created"],
|
||||
"entrypoint": attrs["Config"]["Entrypoint"],
|
||||
"cmd": attrs["Config"]["Cmd"],
|
||||
"restart_policy": attrs["HostConfig"]["RestartPolicy"]
|
||||
}
|
||||
write_audit_log(user, role, "read_detail", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","data":res})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "read_detail", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e),"data":{}})
|
||||
|
||||
# 根路由
|
||||
@app.route("/")
|
||||
def index():
|
||||
return jsonify({"code":400,"msg":"无效操作","list":[],"data":""})
|
||||
|
||||
# ===================== WebSocket 连接鉴权事件(新增) =====================
|
||||
@socketio.on("connect")
|
||||
def handle_connect(auth):
|
||||
user, role, _, auth_ok = verify_socket_auth(auth)
|
||||
if not auth_ok:
|
||||
return False
|
||||
ws_user_map[request.sid] = {
|
||||
"user": user,
|
||||
"role": role
|
||||
}
|
||||
print("WS Connected:", user, role)
|
||||
|
||||
# ===================== WebSocket 交互式TTY实时终端 =====================
|
||||
def stream_terminal_output(sid, sock):
|
||||
"""后台异步读取docker终端输出,兼容两种socket接收方式"""
|
||||
try:
|
||||
while True:
|
||||
chunk = docker_recv(sock, 1024)
|
||||
if not chunk:
|
||||
break
|
||||
text = chunk.decode("utf-8", errors="replace")
|
||||
socketio.emit("terminal_output", {"data": text}, room=sid)
|
||||
except Exception:
|
||||
pass
|
||||
# 流结束自动清理会话
|
||||
if sid in session_exec_map:
|
||||
try:
|
||||
session_exec_map[sid]["socket"].close()
|
||||
except Exception:
|
||||
pass
|
||||
del session_exec_map[sid]
|
||||
|
||||
@socketio.on("terminal_start")
|
||||
def handle_terminal_start(json_data):
|
||||
sid = request.sid
|
||||
# 新增调试输出
|
||||
socketio.emit(
|
||||
"terminal_output",
|
||||
{
|
||||
"data":"[DEBUG] terminal_start reached\r\n"
|
||||
},
|
||||
room=sid
|
||||
)
|
||||
# 替换原verify_auth_sign(),从ws_user_map读取身份
|
||||
info = ws_user_map.get(request.sid)
|
||||
if not info:
|
||||
socketio.emit(
|
||||
"terminal_output",
|
||||
{"data":"身份认证失效\n"},
|
||||
room=request.sid
|
||||
)
|
||||
return
|
||||
user = info["user"]
|
||||
role = info["role"]
|
||||
client_ip = request.remote_addr
|
||||
|
||||
# 权限拦截:非管理员禁止打开交互式终端
|
||||
if role != "admin":
|
||||
socketio.emit("terminal_output", {"data": "Permission denied: 仅管理员可访问交互式终端\n"}, room=sid)
|
||||
write_audit_log(user, role, "terminal_start", "", json_data.get("cid"), "denied", client_ip)
|
||||
return
|
||||
|
||||
cid = json_data.get("cid")
|
||||
if not cid:
|
||||
socketio.emit("terminal_output", {"data": "错误:容器ID不能为空\n"}, room=sid)
|
||||
write_audit_log(user, role, "terminal_start", "", "", "empty_cid", client_ip)
|
||||
return
|
||||
|
||||
# 自动探测容器可用shell,兼容全版本docker-py(exec_create返回dict取Id)
|
||||
target_shell = None
|
||||
for shell in SHELL_CANDIDATES:
|
||||
try:
|
||||
ret = api_cli.exec_create(
|
||||
container=cid,
|
||||
cmd=[shell, "-c", "echo ok"]
|
||||
)
|
||||
test_exec_id = ret["Id"]
|
||||
test_stream = api_cli.exec_start(test_exec_id)
|
||||
test_stream.read()
|
||||
target_shell = shell
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not target_shell:
|
||||
socketio.emit("terminal_output", {"data": "错误:容器内无可用shell(bash/sh/ash)\n"}, room=sid)
|
||||
write_audit_log(user, role, "terminal_start", "", cid, "no_shell", client_ip)
|
||||
return
|
||||
|
||||
try:
|
||||
# 创建交互式TTY会话,统一提取Id兼容所有SDK版本
|
||||
exec_ret = api_cli.exec_create(
|
||||
container=cid,
|
||||
cmd=target_shell,
|
||||
stdin=True,
|
||||
stdout=True,
|
||||
stderr=True,
|
||||
tty=True
|
||||
)
|
||||
exec_id = exec_ret["Id"]
|
||||
sock = api_cli.exec_start(exec_id=exec_id, tty=True, socket=True, detach=False)
|
||||
session_exec_map[sid] = {
|
||||
"cid": cid,
|
||||
"exec_id": exec_id,
|
||||
"socket": sock
|
||||
}
|
||||
socketio.start_background_task(stream_terminal_output, sid, sock)
|
||||
container_name = cli.containers.get(cid).name
|
||||
write_audit_log(user, role, "terminal_start", container_name, cid, "success", client_ip)
|
||||
except Exception as e:
|
||||
socketio.emit("terminal_output", {"data": f"启动终端失败:{str(e)}\n"}, room=sid)
|
||||
write_audit_log(user, role, "terminal_start", "", cid, f"fail:{str(e)}", client_ip)
|
||||
|
||||
@socketio.on("terminal_input")
|
||||
def handle_terminal_input(json_data):
|
||||
"""前端输入字符写入容器标准输入,审计脱敏不记录明文"""
|
||||
sid = request.sid
|
||||
# 替换原verify_auth_sign()
|
||||
info = ws_user_map.get(request.sid)
|
||||
if not info:
|
||||
return
|
||||
user = info["user"]
|
||||
role = info["role"]
|
||||
client_ip = request.remote_addr
|
||||
|
||||
if sid not in session_exec_map:
|
||||
socketio.emit("terminal_output", {"data": "终端会话已断开,请重新打开\n"}, room=sid)
|
||||
return
|
||||
input_text = json_data.get("data", "")
|
||||
sock = session_exec_map[sid]["socket"]
|
||||
sock.send(input_text.encode("utf-8"))
|
||||
cid = session_exec_map[sid]["cid"]
|
||||
# 审计仅标记事件,不再记录明文命令,规避密码泄露
|
||||
write_audit_log(user, role, "terminal_input", "", cid, "command_entered", client_ip)
|
||||
|
||||
@socketio.on("terminal_resize")
|
||||
def handle_terminal_resize(json_data):
|
||||
"""前端xterm窗口大小自适应"""
|
||||
sid = request.sid
|
||||
if sid not in session_exec_map:
|
||||
return
|
||||
exec_id = session_exec_map[sid]["exec_id"]
|
||||
height = int(json_data.get("height", 24))
|
||||
width = int(json_data.get("width", 80))
|
||||
try:
|
||||
api_cli.exec_resize(exec_id=exec_id, height=height, width=width)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 新增主动关闭终端事件(解决切换容器残留TTY)
|
||||
@socketio.on("terminal_close")
|
||||
def handle_terminal_close():
|
||||
sid = request.sid
|
||||
info = ws_user_map.get(request.sid)
|
||||
if not info:
|
||||
return
|
||||
user = info["user"]
|
||||
role = info["role"]
|
||||
client_ip = request.remote_addr
|
||||
|
||||
if sid in session_exec_map:
|
||||
cid = session_exec_map[sid]["cid"]
|
||||
try:
|
||||
session_exec_map[sid]["socket"].close()
|
||||
except Exception:
|
||||
pass
|
||||
del session_exec_map[sid]
|
||||
write_audit_log(user, role, "terminal_close", "", cid, "manual_close", client_ip)
|
||||
|
||||
@socketio.on("disconnect")
|
||||
def handle_disconnect():
|
||||
"""前端断开连接,释放docker socket、清理会话,防止句柄泄漏"""
|
||||
sid = request.sid
|
||||
# 清理ws_user_map会话
|
||||
if sid in ws_user_map:
|
||||
del ws_user_map[sid]
|
||||
info = ws_user_map.get(sid)
|
||||
if not info:
|
||||
user = ""
|
||||
role = ""
|
||||
client_ip = request.remote_addr
|
||||
else:
|
||||
user = info["user"]
|
||||
role = info["role"]
|
||||
client_ip = request.remote_addr
|
||||
|
||||
# 清理TTY终端
|
||||
if sid in session_exec_map:
|
||||
cid = session_exec_map[sid]["cid"]
|
||||
try:
|
||||
session_exec_map[sid]["socket"].close()
|
||||
except Exception:
|
||||
pass
|
||||
del session_exec_map[sid]
|
||||
write_audit_log(user, role, "terminal_close", "", cid, "disconnect", client_ip)
|
||||
# 清理实时日志标记,终止日志循环
|
||||
if sid in log_task_map:
|
||||
del log_task_map[sid]
|
||||
|
||||
# ===================== WebSocket 实时流式日志 docker logs -f(生成器主动释放) =====================
|
||||
def stream_follow_log(sid, cid):
|
||||
generator = None
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
generator = container.logs(stream=True, follow=True)
|
||||
log_task_map[sid] = {"cid": cid, "running": True}
|
||||
for line in generator:
|
||||
task_info = log_task_map.get(sid, {})
|
||||
if not task_info.get("running"):
|
||||
break
|
||||
txt = line.decode("utf-8", errors="replace")
|
||||
socketio.emit("log_output", {"data": txt}, room=sid)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
# 主动关闭日志生成器,释放docker daemon长连接
|
||||
if generator is not None:
|
||||
generator.close()
|
||||
if sid in log_task_map:
|
||||
del log_task_map[sid]
|
||||
|
||||
@socketio.on("log_start")
|
||||
def handle_log_start(json_data):
|
||||
sid = request.sid
|
||||
# 替换verify_auth_sign,读取ws_user_map
|
||||
info = ws_user_map.get(request.sid)
|
||||
if not info:
|
||||
socketio.emit("log_output", {"data": "身份校验失败"}, room=sid)
|
||||
write_audit_log("", "", "log_start", "", "", "auth_failed", request.remote_addr)
|
||||
return
|
||||
user = info["user"]
|
||||
role = info["role"]
|
||||
client_ip = request.remote_addr
|
||||
|
||||
cid = json_data.get("cid")
|
||||
if not cid:
|
||||
socketio.emit("log_output", {"data": "容器ID不能为空"}, room=sid)
|
||||
write_audit_log(user, role, "log_start", "", "", "empty_cid", client_ip)
|
||||
return
|
||||
if role != "admin":
|
||||
socketio.emit("log_output", {"data": "权限不足,仅管理员可查看实时日志"}, room=sid)
|
||||
write_audit_log(user, role, "log_start", "", cid, "denied", client_ip)
|
||||
return
|
||||
socketio.start_background_task(stream_follow_log, sid, cid)
|
||||
container_name = cli.containers.get(cid).name
|
||||
write_audit_log(user, role, "log_start", container_name, cid, "success", client_ip)
|
||||
|
||||
@socketio.on("log_stop")
|
||||
def handle_log_stop():
|
||||
sid = request.sid
|
||||
info = ws_user_map.get(request.sid)
|
||||
if not info:
|
||||
return
|
||||
user = info["user"]
|
||||
role = info["role"]
|
||||
client_ip = request.remote_addr
|
||||
|
||||
if sid in log_task_map:
|
||||
log_task_map[sid]["running"] = False
|
||||
cid = log_task_map[sid]["cid"]
|
||||
write_audit_log(user, role, "log_stop", "", cid, "manual_close", client_ip)
|
||||
|
||||
# 启动入口(eventlet异步,必须socketio.run)
|
||||
if __name__ == "__main__":
|
||||
socketio.run(app, host="0.0.0.0", port=8090, debug=False, allow_unsafe_werkzeug=True)
|
||||
@@ -0,0 +1,100 @@
|
||||
import json
|
||||
import docker
|
||||
from flask import Flask, jsonify
|
||||
|
||||
# 兼容新旧版本
|
||||
try:
|
||||
cli = docker.DockerClient(base_url='unix:///var/run/docker.sock')
|
||||
except AttributeError:
|
||||
cli = docker.Client(base_url='unix:///var/run/docker.sock')
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# 容器列表
|
||||
@app.route("/list")
|
||||
def api_list():
|
||||
arr = []
|
||||
for c in cli.containers.list(all=True):
|
||||
img_name = c.image.tags[0] if c.image.tags else ""
|
||||
arr.append({
|
||||
"id": c.short_id,
|
||||
"full_id": c.id,
|
||||
"name": c.name,
|
||||
"image": img_name,
|
||||
"status": c.status,
|
||||
"ports": str(c.ports)
|
||||
})
|
||||
return jsonify({"code":0,"msg":"success","list":arr})
|
||||
|
||||
# 镜像列表
|
||||
@app.route("/image_list")
|
||||
def api_image_list():
|
||||
arr = []
|
||||
for img in cli.images.list():
|
||||
if not img.tags:
|
||||
continue
|
||||
full_tag = img.tags[0]
|
||||
repo, tag = full_tag.split(":", 1)
|
||||
size_mb = round(img.attrs["Size"] / 1024 / 1024, 1)
|
||||
arr.append({
|
||||
"repo": repo,
|
||||
"tag": tag,
|
||||
"id": img.short_id,
|
||||
"full_id": img.id,
|
||||
"size": f"{size_mb}MB",
|
||||
"created": img.attrs["Created"]
|
||||
})
|
||||
return jsonify({"code":0,"msg":"success","list":arr})
|
||||
|
||||
# 重启容器
|
||||
@app.route("/restart/<cid>")
|
||||
def api_restart(cid):
|
||||
try:
|
||||
cli.containers.get(cid).restart()
|
||||
return jsonify({"code":0,"msg":"容器已重启"})
|
||||
except Exception as e:
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 停止容器
|
||||
@app.route("/stop/<cid>")
|
||||
def api_stop(cid):
|
||||
try:
|
||||
cli.containers.get(cid).stop()
|
||||
return jsonify({"code":0,"msg":"容器已停止"})
|
||||
except Exception as e:
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 删除容器
|
||||
@app.route("/rm/<cid>")
|
||||
def api_rm(cid):
|
||||
try:
|
||||
cli.containers.get(cid).remove(force=True)
|
||||
return jsonify({"code":0,"msg":"容器已强制删除"})
|
||||
except Exception as e:
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 容器日志
|
||||
@app.route("/logs/<cid>/<int:tail_lines>")
|
||||
def api_logs(cid, tail_lines):
|
||||
try:
|
||||
log_bytes = cli.containers.get(cid).logs(tail=tail_lines)
|
||||
log_str = log_bytes.decode("utf-8", errors="ignore")
|
||||
return jsonify({"code":0,"msg":"success","data":log_str})
|
||||
except Exception as e:
|
||||
return jsonify({"code":500,"msg":str(e),"data":""})
|
||||
|
||||
# 网络信息
|
||||
@app.route("/network/<cid>")
|
||||
def api_network(cid):
|
||||
try:
|
||||
info = cli.containers.get(cid).attrs["NetworkSettings"]["Networks"]
|
||||
return jsonify({"code":0,"msg":"success","data":info})
|
||||
except Exception as e:
|
||||
return jsonify({"code":500,"msg":str(e),"data":""})
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return jsonify({"code":400,"msg":"无效操作","list":[],"data":""})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8090, debug=False)
|
||||
@@ -0,0 +1,669 @@
|
||||
# 【强制置顶】eventlet猴子补丁,必须放在所有导入最前面
|
||||
import json
|
||||
import shlex
|
||||
import time
|
||||
import hashlib
|
||||
import hmac
|
||||
import docker
|
||||
from flask import Flask, jsonify, request
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
# 全局签名密钥,前后端统一,生产环境使用环境变量注入
|
||||
SIGN_SECRET = b"DockerAdminPlatform2026SecretKey123456789"
|
||||
# 签名有效期 300秒(5分钟)
|
||||
SIGN_EXPIRE = 300
|
||||
|
||||
# 兼容新旧docker-py客户端
|
||||
try:
|
||||
cli = docker.DockerClient(base_url='unix:///var/run/docker.sock')
|
||||
api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock')
|
||||
except AttributeError:
|
||||
cli = docker.Client(base_url='unix:///var/run/docker.sock')
|
||||
api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock')
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'docker-terminal-secret-2026'
|
||||
|
||||
# WebSocket心跳保活,防止Nginx/反向代理断连
|
||||
socketio = SocketIO(
|
||||
app,
|
||||
cors_allowed_origins="*",
|
||||
async_mode="threading",
|
||||
ping_timeout=60,
|
||||
ping_interval=25
|
||||
)
|
||||
|
||||
# 全局会话存储
|
||||
session_exec_map = {} # sid => TTY终端流信息
|
||||
log_task_map = {} # sid => {"cid": "", "running": True}
|
||||
registry_config = {
|
||||
"repo_name": "",
|
||||
"repo_addr": "",
|
||||
"repo_user": "",
|
||||
"repo_pwd": ""
|
||||
}
|
||||
# 多系统shell兼容列表(Ubuntu/Alpine/标准Linux)
|
||||
SHELL_CANDIDATES = [
|
||||
"/bin/bash",
|
||||
"/usr/bin/bash",
|
||||
"/bin/sh",
|
||||
"/bin/ash"
|
||||
]
|
||||
|
||||
# ===================== 工具函数1:docker socket兼容recv =====================
|
||||
def docker_recv(sock, size=1024):
|
||||
try:
|
||||
return sock.recv(size)
|
||||
except Exception:
|
||||
try:
|
||||
return sock._sock.recv(size)
|
||||
except Exception:
|
||||
return b""
|
||||
|
||||
# ===================== 工具函数2:操作审计日志(终端输入脱敏,不存明文密码) =====================
|
||||
def write_audit_log(user, role, action, target_name, target_id, result, client_ip):
|
||||
"""企业审计记录,可对接数据库docker_audit_log表"""
|
||||
# 终端输入仅标记事件,不记录明文命令,规避密码泄露风险
|
||||
if action == "terminal_input":
|
||||
result = "command_entered"
|
||||
log_item = {
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"user": user,
|
||||
"role": role,
|
||||
"action": action,
|
||||
"container_name": target_name,
|
||||
"container_id": target_id,
|
||||
"result": result,
|
||||
"client_ip": client_ip
|
||||
}
|
||||
# 生产环境替换为mysql insert语句
|
||||
print(json.dumps(log_item, ensure_ascii=False))
|
||||
|
||||
# ===================== 工具函数3:HMAC签名鉴权,防Header伪造 =====================
|
||||
def verify_auth_sign():
|
||||
"""
|
||||
校验前端PHP生成的HMAC签名,防止伪造X-Role:admin
|
||||
请求Header要求:
|
||||
X-User: 用户名
|
||||
X-Role: admin/user/guest
|
||||
X-Time: 当前时间戳(秒)
|
||||
X-Sign: hmac-sha256签名
|
||||
返回 (user, role, client_ip, True/False)
|
||||
"""
|
||||
user = request.headers.get("X-User", "anonymous")
|
||||
role = request.headers.get("X-Role", "guest")
|
||||
ts_str = request.headers.get("X-Time", "0")
|
||||
sign = request.headers.get("X-Sign", "")
|
||||
client_ip = request.headers.get("X-Client-Ip", request.remote_addr)
|
||||
try:
|
||||
ts = int(ts_str)
|
||||
except ValueError:
|
||||
return user, role, client_ip, False
|
||||
# 校验签名有效期
|
||||
now = int(time.time())
|
||||
if abs(now - ts) > SIGN_EXPIRE:
|
||||
return user, role, client_ip, False
|
||||
# 校验HMAC签名
|
||||
raw_data = f"{user}|{role}|{ts}".encode("utf-8")
|
||||
calc_sign = hmac.new(SIGN_SECRET, raw_data, hashlib.sha256).hexdigest()
|
||||
if calc_sign != sign:
|
||||
return user, role, client_ip, False
|
||||
return user, role, client_ip, True
|
||||
|
||||
# ===================== 工具函数4:环境变量脱敏,屏蔽密码/密钥 =====================
|
||||
def safe_env_filter(env_list):
|
||||
safe_env = []
|
||||
sensitive_keywords = ["PASS", "PASSWORD", "TOKEN", "SECRET", "KEY", "CREDENTIAL"]
|
||||
for item in env_list:
|
||||
equal_pos = item.find("=")
|
||||
if equal_pos == -1:
|
||||
safe_env.append(item)
|
||||
continue
|
||||
key = item[:equal_pos]
|
||||
val = item[equal_pos+1:]
|
||||
if any(word in key.upper() for word in sensitive_keywords):
|
||||
safe_env.append(f"{key}=******")
|
||||
else:
|
||||
safe_env.append(item)
|
||||
return safe_env
|
||||
|
||||
# ===================== 全部HTTP业务接口 =====================
|
||||
# 1. 容器列表(端口预拼接字符串,前端直接渲染,无需formatPorts)
|
||||
@app.route("/list")
|
||||
def api_list():
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "list_container", "", "", "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
arr = []
|
||||
for c in cli.containers.list(all=True):
|
||||
img_name = c.image.tags[0] if c.image.tags else ""
|
||||
port_str_arr = []
|
||||
ports_raw = c.ports
|
||||
for c_port, host_list in ports_raw.items():
|
||||
if not host_list:
|
||||
continue
|
||||
for host_item in host_list:
|
||||
host_port = host_item.get("HostPort", "")
|
||||
port_str_arr.append(f"{host_port}:{c_port.split('/')[0]}")
|
||||
port_display = ",".join(port_str_arr) if port_str_arr else "-"
|
||||
arr.append({
|
||||
"id": c.short_id,
|
||||
"full_id": c.id,
|
||||
"name": c.name,
|
||||
"image": img_name,
|
||||
"status": c.status,
|
||||
"ports": port_display
|
||||
})
|
||||
write_audit_log(user, role, "list_container", "", "", "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","list":arr})
|
||||
|
||||
# 2. 镜像列表
|
||||
@app.route("/image_list")
|
||||
def api_image_list():
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "list_image", "", "", "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
arr = []
|
||||
for img in cli.images.list():
|
||||
if not img.tags:
|
||||
continue
|
||||
full_tag = img.tags[0]
|
||||
repo, tag = full_tag.split(":", 1)
|
||||
size_mb = round(img.attrs["Size"] / 1024 / 1024, 1)
|
||||
arr.append({
|
||||
"repo": repo,
|
||||
"tag": tag,
|
||||
"id": img.short_id,
|
||||
"full_id": img.id,
|
||||
"size": f"{size_mb}MB",
|
||||
"created": img.attrs["Created"]
|
||||
})
|
||||
write_audit_log(user, role, "list_image", "", "", "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","list":arr})
|
||||
|
||||
# 3. 删除镜像
|
||||
@app.route("/remove_image/<image_id>")
|
||||
def api_remove_image(image_id):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "remove_image", "", image_id, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "remove_image", "", image_id, "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可删除镜像"})
|
||||
try:
|
||||
img = cli.images.get(image_id)
|
||||
cli.images.remove(image_id, force=True)
|
||||
write_audit_log(user, role, "remove_image", img.tags[0] if img.tags else "", image_id, "success", ip)
|
||||
return jsonify({"code":0,"msg":"镜像已强制删除"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "remove_image", "", image_id, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 4. 重启容器
|
||||
@app.route("/restart/<cid>")
|
||||
def api_restart(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "restart", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "restart", "", cid, "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
container.restart()
|
||||
write_audit_log(user, role, "restart", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"容器已重启"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "restart", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 5. 停止容器
|
||||
@app.route("/stop/<cid>")
|
||||
def api_stop(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "stop", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "stop", "", cid, "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
container.stop()
|
||||
write_audit_log(user, role, "stop", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"容器已停止"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "stop", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 6. 删除容器
|
||||
@app.route("/rm/<cid>")
|
||||
def api_rm(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "rm", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "rm", "", cid, "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可删除容器"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
container.remove(force=True)
|
||||
write_audit_log(user, role, "rm", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"容器已强制删除"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "rm", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 7. 一次性拉取日志(兼容旧页面单次查看)
|
||||
@app.route("/logs/<cid>/<int:tail_lines>")
|
||||
def api_logs(cid, tail_lines):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "read_log", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
log_bytes = container.logs(tail=tail_lines)
|
||||
log_str = log_bytes.decode("utf-8", errors="replace")
|
||||
write_audit_log(user, role, "read_log", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","data":log_str})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "read_log", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e),"data":""})
|
||||
|
||||
# 8. 容器网络详情
|
||||
@app.route("/network/<cid>")
|
||||
def api_network(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "read_network", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
info = container.attrs["NetworkSettings"]["Networks"]
|
||||
write_audit_log(user, role, "read_network", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","data":json.dumps(info,indent=2)})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "read_network", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e),"data":""})
|
||||
|
||||
# 9. 短连接单次执行命令(旧弹窗兼容,非实时终端)
|
||||
@app.route("/exec/<cid>", methods=["POST"])
|
||||
def api_exec(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "single_exec", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "single_exec", "", cid, "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可执行命令"})
|
||||
try:
|
||||
data = request.get_json()
|
||||
cmd = data.get("cmd", "")
|
||||
if not cmd:
|
||||
return jsonify({"code":400,"msg":"命令不能为空","data":""})
|
||||
cmd_list = shlex.split(cmd)
|
||||
container = cli.containers.get(cid)
|
||||
exec_obj = container.exec_run(cmd_list, stdout=True, stderr=True)
|
||||
output = exec_obj.output.decode("utf-8", errors="replace")
|
||||
write_audit_log(user, role, "single_exec", container.name, cid, "command_executed", ip)
|
||||
return jsonify({"code":0,"msg":"success","data":output})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "single_exec", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e),"data":""})
|
||||
|
||||
# 10. 创建容器(端口标准格式 + 前端可配置CPU/内存)
|
||||
@app.route("/create", methods=["POST"])
|
||||
def api_create():
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "create_container", "", "", "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "create_container", "", "", "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可创建容器"})
|
||||
try:
|
||||
data = request.get_json()
|
||||
name = data.get("name", "")
|
||||
image = data.get("image")
|
||||
cmd = data.get("cmd")
|
||||
ports_input = data.get("ports", {})
|
||||
# 修复:docker标准端口字典格式 {内部端口:宿主机端口}
|
||||
port_bind = {}
|
||||
for inner_port, host_port in ports_input.items():
|
||||
port_bind[f"{inner_port}/tcp"] = int(host_port)
|
||||
# 前端可配置资源限制,默认1核512M
|
||||
cpu_core = int(data.get("cpu", 1))
|
||||
memory_limit = data.get("memory", "512m")
|
||||
nano_cpus = cpu_core * 1000000000
|
||||
container = cli.containers.run(
|
||||
image=image,
|
||||
name=name if name else None,
|
||||
command=cmd if cmd else None,
|
||||
ports=port_bind,
|
||||
detach=True,
|
||||
restart_policy={"Name": "always"},
|
||||
mem_limit=memory_limit,
|
||||
nano_cpus=nano_cpus
|
||||
)
|
||||
write_audit_log(user, role, "create_container", container.name, container.id, "success", ip)
|
||||
return jsonify({"code":0,"msg":f"容器创建成功,ID:{container.short_id}"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "create_container", "", "", f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 11. 登录私有镜像仓库
|
||||
@app.route("/bind_repo", methods=["POST"])
|
||||
def api_bind_repo():
|
||||
global registry_config
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "repo_login", "", "", "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
if role != "admin":
|
||||
write_audit_log(user, role, "repo_login", "", "", "denied", ip)
|
||||
return jsonify({"code":403,"msg":"权限不足,仅管理员可登录镜像仓库"})
|
||||
try:
|
||||
data = request.get_json()
|
||||
repo_name = data.get("repo_name","")
|
||||
repo_addr = data.get("repo_addr")
|
||||
repo_user = data.get("repo_user","")
|
||||
repo_pwd = data.get("repo_pwd","")
|
||||
api_cli.login(username=repo_user, password=repo_pwd, registry=repo_addr)
|
||||
registry_config["repo_name"] = repo_name
|
||||
registry_config["repo_addr"] = repo_addr
|
||||
registry_config["repo_user"] = repo_user
|
||||
registry_config["repo_pwd"] = repo_pwd
|
||||
write_audit_log(user, role, "repo_login", repo_addr, "", "success", ip)
|
||||
return jsonify({"code":0,"msg":"镜像仓库登录绑定成功"})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "repo_login", repo_addr, "", f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e)})
|
||||
|
||||
# 修复:stats接口online_cpus兜底兼容,无KeyError
|
||||
@app.route("/stats/<cid>")
|
||||
def api_stats(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "read_stats", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
raw = container.stats(stream=False)
|
||||
# CPU兼容兜底,不存在online_cpus自动计算
|
||||
cpu_stats = raw["cpu_stats"]
|
||||
online_cpus = cpu_stats.get("online_cpus", len(cpu_stats["cpu_usage"].get("percpu_usage", [])) or 1)
|
||||
cpu_delta = cpu_stats["cpu_usage"]["total_usage"] - raw["precpu_stats"]["cpu_usage"]["total_usage"]
|
||||
system_delta = cpu_stats["system_cpu_usage"] - raw["precpu_stats"]["system_cpu_usage"]
|
||||
cpu_percent = 0.0
|
||||
if system_delta > 0 and cpu_delta > 0:
|
||||
cpu_percent = round((cpu_delta / system_delta) * online_cpus * 100, 1)
|
||||
# 内存计算
|
||||
mem_usage = raw["memory_stats"]["usage"] / 1024 / 1024
|
||||
mem_limit = raw["memory_stats"]["limit"] / 1024 / 1024
|
||||
mem_percent = round((mem_usage / mem_limit) * 100, 1) if mem_limit > 0 else 0
|
||||
# 网络流量
|
||||
net_rx = 0
|
||||
net_tx = 0
|
||||
for net in raw["networks"].values():
|
||||
net_rx += net["rx_bytes"] / 1024 / 1024
|
||||
net_tx += net["tx_bytes"] / 1024 / 1024
|
||||
res = {
|
||||
"cpu": cpu_percent,
|
||||
"mem_mb": round(mem_usage, 1),
|
||||
"mem_limit_mb": round(mem_limit, 1),
|
||||
"mem_percent": mem_percent,
|
||||
"net_rx_mb": round(net_rx, 1),
|
||||
"net_tx_mb": round(net_tx, 1)
|
||||
}
|
||||
write_audit_log(user, role, "read_stats", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","data":res})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "read_stats", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e),"data":{}})
|
||||
|
||||
# 修复:detail接口ENV脱敏,多网卡IP兼容
|
||||
@app.route("/detail/<cid>")
|
||||
def api_detail(cid):
|
||||
user, role, ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
write_audit_log(user, role, "read_detail", "", cid, "auth_failed", ip)
|
||||
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
attrs = container.attrs
|
||||
nets = attrs["NetworkSettings"]["Networks"]
|
||||
ip_map = {}
|
||||
for net_name, net_info in nets.items():
|
||||
ip_map[net_name] = net_info.get("IPAddress", "")
|
||||
# 敏感环境变量脱敏
|
||||
raw_env = attrs["Config"].get("Env", [])
|
||||
safe_env = safe_env_filter(raw_env)
|
||||
res = {
|
||||
"network_ips": ip_map,
|
||||
"mounts": attrs["Mounts"],
|
||||
"env": safe_env,
|
||||
"created": attrs["Created"],
|
||||
"entrypoint": attrs["Config"]["Entrypoint"],
|
||||
"cmd": attrs["Config"]["Cmd"],
|
||||
"restart_policy": attrs["HostConfig"]["RestartPolicy"]
|
||||
}
|
||||
write_audit_log(user, role, "read_detail", container.name, cid, "success", ip)
|
||||
return jsonify({"code":0,"msg":"success","data":res})
|
||||
except Exception as e:
|
||||
write_audit_log(user, role, "read_detail", "", cid, f"fail:{str(e)}", ip)
|
||||
return jsonify({"code":500,"msg":str(e),"data":{}})
|
||||
|
||||
# 根路由
|
||||
@app.route("/")
|
||||
def index():
|
||||
return jsonify({"code":400,"msg":"无效操作","list":[],"data":""})
|
||||
|
||||
# ===================== WebSocket 交互式TTY实时终端 =====================
|
||||
def stream_terminal_output(sid, sock):
|
||||
"""后台异步读取docker终端输出,兼容两种socket接收方式"""
|
||||
try:
|
||||
while True:
|
||||
chunk = docker_recv(sock, 1024)
|
||||
if not chunk:
|
||||
break
|
||||
text = chunk.decode("utf-8", errors="replace")
|
||||
socketio.emit("terminal_output", {"data": text}, room=sid)
|
||||
except Exception:
|
||||
pass
|
||||
# 流结束自动清理会话
|
||||
if sid in session_exec_map:
|
||||
try:
|
||||
session_exec_map[sid]["socket"].close()
|
||||
except Exception:
|
||||
pass
|
||||
del session_exec_map[sid]
|
||||
|
||||
@socketio.on("terminal_start")
|
||||
def handle_terminal_start(json_data):
|
||||
sid = request.sid
|
||||
user, role, client_ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
socketio.emit("terminal_output", {"data": "Permission denied: 身份校验失败\n"}, room=sid)
|
||||
write_audit_log(user, role, "terminal_start", "", json_data.get("cid"), "auth_failed", client_ip)
|
||||
return
|
||||
# 权限拦截:非管理员禁止打开交互式终端
|
||||
if role != "admin":
|
||||
socketio.emit("terminal_output", {"data": "Permission denied: 仅管理员可访问交互式终端\n"}, room=sid)
|
||||
write_audit_log(user, role, "terminal_start", "", json_data.get("cid"), "denied", client_ip)
|
||||
return
|
||||
|
||||
cid = json_data.get("cid")
|
||||
if not cid:
|
||||
socketio.emit("terminal_output", {"data": "错误:容器ID不能为空\n"}, room=sid)
|
||||
write_audit_log(user, role, "terminal_start", "", "", "empty_cid", client_ip)
|
||||
return
|
||||
|
||||
# 自动探测容器可用shell,兼容全版本docker-py(exec_create返回dict取Id)
|
||||
target_shell = None
|
||||
for shell in SHELL_CANDIDATES:
|
||||
try:
|
||||
ret = api_cli.exec_create(
|
||||
container=cid,
|
||||
cmd=[shell, "-c", "echo ok"]
|
||||
)
|
||||
test_exec_id = ret["Id"]
|
||||
test_stream = api_cli.exec_start(test_exec_id)
|
||||
test_stream.read()
|
||||
target_shell = shell
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not target_shell:
|
||||
socketio.emit("terminal_output", {"data": "错误:容器内无可用shell(bash/sh/ash)\n"}, room=sid)
|
||||
write_audit_log(user, role, "terminal_start", "", cid, "no_shell", client_ip)
|
||||
return
|
||||
|
||||
try:
|
||||
# 创建交互式TTY会话,统一提取Id兼容所有SDK版本
|
||||
exec_ret = api_cli.exec_create(
|
||||
container=cid,
|
||||
cmd=target_shell,
|
||||
stdin=True,
|
||||
stdout=True,
|
||||
stderr=True,
|
||||
tty=True
|
||||
)
|
||||
exec_id = exec_ret["Id"]
|
||||
sock = api_cli.exec_start(exec_id=exec_id, tty=True, socket=True, detach=False)
|
||||
session_exec_map[sid] = {
|
||||
"cid": cid,
|
||||
"exec_id": exec_id,
|
||||
"socket": sock
|
||||
}
|
||||
socketio.start_background_task(stream_terminal_output, sid, sock)
|
||||
container_name = cli.containers.get(cid).name
|
||||
write_audit_log(user, role, "terminal_start", container_name, cid, "success", client_ip)
|
||||
except Exception as e:
|
||||
socketio.emit("terminal_output", {"data": f"启动终端失败:{str(e)}\n"}, room=sid)
|
||||
write_audit_log(user, role, "terminal_start", "", cid, f"fail:{str(e)}", client_ip)
|
||||
|
||||
@socketio.on("terminal_input")
|
||||
def handle_terminal_input(json_data):
|
||||
"""前端输入字符写入容器标准输入,审计脱敏不记录明文"""
|
||||
sid = request.sid
|
||||
user, role, client_ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
socketio.emit("terminal_output", {"data": "身份校验失败\n"}, room=sid)
|
||||
return
|
||||
if sid not in session_exec_map:
|
||||
socketio.emit("terminal_output", {"data": "终端会话已断开,请重新打开\n"}, room=sid)
|
||||
return
|
||||
input_text = json_data.get("data", "")
|
||||
sock = session_exec_map[sid]["socket"]
|
||||
sock.send(input_text.encode("utf-8"))
|
||||
cid = session_exec_map[sid]["cid"]
|
||||
# 审计仅标记事件,不再记录明文命令,规避密码泄露
|
||||
write_audit_log(user, role, "terminal_input", "", cid, "command_entered", client_ip)
|
||||
|
||||
@socketio.on("terminal_resize")
|
||||
def handle_terminal_resize(json_data):
|
||||
"""前端xterm窗口大小自适应"""
|
||||
sid = request.sid
|
||||
if sid not in session_exec_map:
|
||||
return
|
||||
exec_id = session_exec_map[sid]["exec_id"]
|
||||
height = int(json_data.get("height", 24))
|
||||
width = int(json_data.get("width", 80))
|
||||
try:
|
||||
api_cli.exec_resize(exec_id=exec_id, height=height, width=width)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 新增主动关闭终端事件(解决切换容器残留TTY)
|
||||
@socketio.on("terminal_close")
|
||||
def handle_terminal_close():
|
||||
sid = request.sid
|
||||
user, role, client_ip, auth_ok = verify_auth_sign()
|
||||
if sid in session_exec_map:
|
||||
cid = session_exec_map[sid]["cid"]
|
||||
try:
|
||||
session_exec_map[sid]["socket"].close()
|
||||
except Exception:
|
||||
pass
|
||||
del session_exec_map[sid]
|
||||
write_audit_log(user, role, "terminal_close", "", cid, "manual_close", client_ip)
|
||||
|
||||
@socketio.on("disconnect")
|
||||
def handle_disconnect():
|
||||
"""前端断开连接,释放docker socket、清理会话,防止句柄泄漏"""
|
||||
sid = request.sid
|
||||
user, role, client_ip, auth_ok = verify_auth_sign()
|
||||
# 清理TTY终端
|
||||
if sid in session_exec_map:
|
||||
cid = session_exec_map[sid]["cid"]
|
||||
try:
|
||||
session_exec_map[sid]["socket"].close()
|
||||
except Exception:
|
||||
pass
|
||||
del session_exec_map[sid]
|
||||
write_audit_log(user, role, "terminal_close", "", cid, "disconnect", client_ip)
|
||||
# 清理实时日志标记,终止日志循环
|
||||
if sid in log_task_map:
|
||||
del log_task_map[sid]
|
||||
|
||||
# ===================== WebSocket 实时流式日志 docker logs -f(生成器主动释放) =====================
|
||||
def stream_follow_log(sid, cid):
|
||||
generator = None
|
||||
try:
|
||||
container = cli.containers.get(cid)
|
||||
generator = container.logs(stream=True, follow=True)
|
||||
log_task_map[sid] = {"cid": cid, "running": True}
|
||||
for line in generator:
|
||||
task_info = log_task_map.get(sid, {})
|
||||
if not task_info.get("running"):
|
||||
break
|
||||
txt = line.decode("utf-8", errors="replace")
|
||||
socketio.emit("log_output", {"data": txt}, room=sid)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
# 主动关闭日志生成器,释放docker daemon长连接
|
||||
if generator is not None:
|
||||
generator.close()
|
||||
if sid in log_task_map:
|
||||
del log_task_map[sid]
|
||||
|
||||
@socketio.on("log_start")
|
||||
def handle_log_start(json_data):
|
||||
sid = request.sid
|
||||
user, role, client_ip, auth_ok = verify_auth_sign()
|
||||
if not auth_ok:
|
||||
socketio.emit("log_output", {"data": "身份校验失败"}, room=sid)
|
||||
write_audit_log(user, role, "log_start", "", "", "auth_failed", client_ip)
|
||||
return
|
||||
cid = json_data.get("cid")
|
||||
if not cid:
|
||||
socketio.emit("log_output", {"data": "容器ID不能为空"}, room=sid)
|
||||
write_audit_log(user, role, "log_start", "", "", "empty_cid", client_ip)
|
||||
return
|
||||
if role != "admin":
|
||||
socketio.emit("log_output", {"data": "权限不足,仅管理员可查看实时日志"}, room=sid)
|
||||
write_audit_log(user, role, "log_start", "", cid, "denied", client_ip)
|
||||
return
|
||||
socketio.start_background_task(stream_follow_log, sid, cid)
|
||||
container_name = cli.containers.get(cid).name
|
||||
write_audit_log(user, role, "log_start", container_name, cid, "success", client_ip)
|
||||
|
||||
@socketio.on("log_stop")
|
||||
def handle_log_stop():
|
||||
sid = request.sid
|
||||
user, role, client_ip, auth_ok = verify_auth_sign()
|
||||
if sid in log_task_map:
|
||||
log_task_map[sid]["running"] = False
|
||||
cid = log_task_map[sid]["cid"]
|
||||
write_audit_log(user, role, "log_stop", "", cid, "manual_close", client_ip)
|
||||
|
||||
# 启动入口(eventlet异步,必须socketio.run)
|
||||
if __name__ == "__main__":
|
||||
socketio.run(app, host="0.0.0.0", port=8090, debug=False, allow_unsafe_werkzeug=True)
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
while true; do
|
||||
nc -l 127.0.0.1 9999 <<EOF
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
$(docker ps -a --format '{{.Names}}|{{.ID}}|{{.Image}}|{{.Status}}|{{.Ports}}')
|
||||
EOF
|
||||
done
|
||||
@@ -25,28 +25,58 @@ function getDbConn() {
|
||||
|
||||
// CSV导出接口(支持指定历史日期下载,不输出JSON头)
|
||||
if ($act === "export_csv") {
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
$conn = getDbConn();
|
||||
mysqli_query($conn, "SET time_zone = '+08:00'");
|
||||
if (!$conn) {
|
||||
header("Content-Type:text/html;charset=utf-8");
|
||||
echo "数据库连接失败:" . mysqli_connect_error();
|
||||
exit;
|
||||
}
|
||||
// 接收自定义日期参数,不传则取今日
|
||||
$targetDate = $_GET['date'] ?? date("Y-m-d");
|
||||
// 简单日期格式校验 Y-m-d
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $targetDate)) {
|
||||
|
||||
$singleDate = $_GET['date'] ?? '';
|
||||
$startDate = $_GET['start'] ?? '';
|
||||
$endDate = $_GET['end'] ?? '';
|
||||
$where = "";
|
||||
$fileName = "";
|
||||
|
||||
if (!empty($startDate) && !empty($endDate)) {
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
|
||||
header("Content-Type:application/json;charset=utf-8");
|
||||
echo json_encode(["code"=>400,"msg"=>"日期格式错误,请使用 Y-m-d,例如 2026-07-01"], JSON_UNESCAPED_UNICODE);
|
||||
echo json_encode(["code"=>400,"msg"=>"日期格式错误"], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
if ($startDate > $endDate) {
|
||||
header("Content-Type:application/json;charset=utf-8");
|
||||
echo json_encode(["code"=>400,"msg"=>"开始日期不能大于结束日期"], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
$where = "receive_time >= '$startDate 00:00:00' AND receive_time <= '$endDate 23:59:59'";
|
||||
$fileName = "告警报表_{$startDate}_至_{$endDate}.csv";
|
||||
} elseif (!empty($singleDate)) {
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $singleDate)) {
|
||||
header("Content-Type:application/json;charset=utf-8");
|
||||
echo json_encode(["code"=>400,"msg"=>"日期格式错误"], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
$where = "DATE(receive_time) = '$singleDate'";
|
||||
$fileName = "告警报表_{$singleDate}.csv";
|
||||
} else {
|
||||
$today = date("Y-m-d");
|
||||
$where = "DATE(receive_time) = '$today'";
|
||||
$fileName = "告警报表_今日_{$today}.csv";
|
||||
}
|
||||
|
||||
// 调试:打印where条件,访问接口先看这个
|
||||
// echo "WHERE条件:".$where;exit;
|
||||
|
||||
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
|
||||
FROM alert_log WHERE DATE(receive_time) = '$targetDate' ORDER BY receive_time DESC";
|
||||
FROM alert_log WHERE {$where} ORDER BY receive_time DESC";
|
||||
$res = mysqli_query($conn, $sql);
|
||||
|
||||
ob_clean();
|
||||
header("Content-Type: text/csv; charset=utf-8");
|
||||
header("Content-Disposition: attachment; filename=告警报表_" . $targetDate . ".csv");
|
||||
header("Content-Disposition: attachment; filename={$fileName}");
|
||||
echo "\xEF\xBB\xBF";
|
||||
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
|
||||
echo implode(",", $header) . "\r\n";
|
||||
@@ -69,7 +99,6 @@ if ($act === "export_csv") {
|
||||
mysqli_close($conn);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 所有JSON接口统一返回头
|
||||
header("Content-Type:application/json;charset=utf-8");
|
||||
$conn = getDbConn();
|
||||
@@ -322,13 +322,25 @@ button.normal:hover{
|
||||
|
||||
<!-- 9 导出CSV报表(带日历选择) -->
|
||||
<div class="card">
|
||||
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表(支持历史日期)</h3>
|
||||
<div class="row">
|
||||
<label>选择日期:</label>
|
||||
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表</h3>
|
||||
|
||||
<!-- 第一组:单日导出(原有功能保留) -->
|
||||
<div class="row" style="margin-bottom:12px;gap:10px;align-items:center;flex-wrap:wrap;">
|
||||
<label>单日导出:</label>
|
||||
<input type="date" id="csv_date" placeholder="留空导出今日">
|
||||
<button class="btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>下载对应日期报表</button>
|
||||
<button class="btn-success" onclick="exportCsvSingle()"><i class="fa fa-download"></i>下载单日报表</button>
|
||||
</div>
|
||||
<div class="tip">不选择日期默认导出今日告警;选择历史日期可下载过往报表</div>
|
||||
<div class="tip" style="margin-bottom:16px;">不选择日期默认导出今日告警;选择历史日期下载单日报表</div>
|
||||
|
||||
<!-- 第二组:区间导出(新增) -->
|
||||
<div class="row" style="gap:10px;align-items:center;flex-wrap:wrap;">
|
||||
<label>区间导出:</label>
|
||||
<input type="date" id="csv_start">
|
||||
<span>~</span>
|
||||
<input type="date" id="csv_end">
|
||||
<button class="btn-primary" onclick="exportCsvRange()"><i class="fa fa-download"></i>下载区间报表</button>
|
||||
</div>
|
||||
<div class="tip">填写开始、结束日期,导出该时间段全部告警数据</div>
|
||||
</div>
|
||||
|
||||
<!-- 返回结果输出 -->
|
||||
@@ -397,8 +409,8 @@ function simpleReq(act){
|
||||
})
|
||||
}
|
||||
|
||||
// 导出CSV 支持日历选择日期
|
||||
function exportCsv(){
|
||||
// 单日导出
|
||||
function exportCsvSingle(){
|
||||
const dateVal = document.getElementById("csv_date").value.trim();
|
||||
let url = `${apiUrl}?act=export_csv`;
|
||||
if(dateVal){
|
||||
@@ -406,6 +418,23 @@ function exportCsv(){
|
||||
}
|
||||
window.open(url,"_blank");
|
||||
}
|
||||
|
||||
// 区间导出
|
||||
function exportCsvRange(){
|
||||
const startDate = document.getElementById("csv_start").value.trim();
|
||||
const endDate = document.getElementById("csv_end").value.trim();
|
||||
if(!startDate || !endDate){
|
||||
alert("请同时选择开始日期和结束日期");
|
||||
return;
|
||||
}
|
||||
if(startDate > endDate){
|
||||
alert("开始日期不能晚于结束日期");
|
||||
return;
|
||||
}
|
||||
let url = `${apiUrl}?act=export_csv`;
|
||||
url += `&start=${encodeURIComponent(startDate)}&end=${encodeURIComponent(endDate)}`;
|
||||
window.open(url,"_blank");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,528 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=utf-8");
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
|
||||
header("Access-Control-Allow-Headers: Content-Type");
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
echo json_encode(['code' => 0, 'msg' => 'ok']);
|
||||
exit;
|
||||
}
|
||||
session_start();
|
||||
$expire = 1800;
|
||||
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time']) > $expire) {
|
||||
echo json_encode(['code' => 401, 'msg' => '登录失效']);
|
||||
exit;
|
||||
}
|
||||
$loginUid = $_SESSION['user_id'];
|
||||
|
||||
// 工单业务库 monitor
|
||||
$dbWorkHost = '10.150.117.190';
|
||||
$dbWorkUser = 'root';
|
||||
$dbWorkPwd = 'hp93000';
|
||||
$dbWorkName = 'monitor';
|
||||
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
|
||||
if (!$connWork) {
|
||||
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
|
||||
exit;
|
||||
}
|
||||
mysqli_set_charset($connWork, 'utf8mb4');
|
||||
|
||||
// 配置库 alert_mail_stat(告警、通知配置统一此处)
|
||||
$dbConfHost = "10.150.117.190";
|
||||
$dbConfUser = "root";
|
||||
$dbConfPwd = "hp93000";
|
||||
$dbConfName = "alert_mail_stat";
|
||||
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
|
||||
if(!$connConf){
|
||||
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
|
||||
exit;
|
||||
}
|
||||
mysqli_set_charset($connConf, "utf8mb4");
|
||||
|
||||
// 获取当前用户角色权限
|
||||
$isAdmin = 0;
|
||||
$permList = "";
|
||||
$roleId = 0;
|
||||
$roleSql = "SELECT r.is_admin, r.perm_list, u.role_id FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
|
||||
$stmtRole = mysqli_prepare($connWork, $roleSql);
|
||||
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
|
||||
mysqli_stmt_execute($stmtRole);
|
||||
$roleRes = mysqli_stmt_get_result($stmtRole);
|
||||
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
|
||||
if ($roleRow) {
|
||||
$isAdmin = intval($roleRow['is_admin']);
|
||||
$permList = $roleRow['perm_list'];
|
||||
$roleId = intval($roleRow['role_id'] ?? 0);
|
||||
}
|
||||
|
||||
// 读取当前角色页面权限列表(用于分发工单权限判断)
|
||||
$allowPages = [];
|
||||
if ($roleId > 0) {
|
||||
$permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = ?";
|
||||
$stmtPerm = mysqli_prepare($connWork, $permSql);
|
||||
mysqli_stmt_bind_param($stmtPerm, 'i', $roleId);
|
||||
mysqli_stmt_execute($stmtPerm);
|
||||
$pRes = mysqli_stmt_get_result($stmtPerm);
|
||||
while ($p = mysqli_fetch_assoc($pRes)) {
|
||||
$allowPages[] = $p['page_key'];
|
||||
}
|
||||
}
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$post = json_decode(file_get_contents("php://input"), true) ?: [];
|
||||
|
||||
// ===================== 工单列表查询 =====================
|
||||
if ($action === "get_list") {
|
||||
$page = intval($_GET['page'] ?? 1);
|
||||
$size = intval($_GET['size'] ?? 20);
|
||||
$offset = ($page - 1) * $size;
|
||||
$status = trim($_GET['status'] ?? '');
|
||||
$keyword = trim($_GET['keyword'] ?? '');
|
||||
|
||||
$where = [];
|
||||
$param = [];
|
||||
$paramType = '';
|
||||
|
||||
// 全部用户查看全部工单,不再按uid过滤
|
||||
$whereSql = "1=1";
|
||||
|
||||
if ($status !== '') {
|
||||
$where[] = "status = ?";
|
||||
$param[] = $status;
|
||||
$paramType .= 'i';
|
||||
}
|
||||
if ($keyword !== '') {
|
||||
$where[] = "(title LIKE ? OR content LIKE ?)";
|
||||
$param[] = "%$keyword%";
|
||||
$param[] = "%$keyword%";
|
||||
$paramType .= 'ss';
|
||||
}
|
||||
|
||||
if (!empty($where)) {
|
||||
$whereSql = "WHERE " . implode(" AND ", $where);
|
||||
}
|
||||
|
||||
// 分页列表,LEFT JOIN 认领人姓名
|
||||
$listSql = "SELECT wo.*, uu.real_name claim_name
|
||||
FROM work_order wo
|
||||
LEFT JOIN sys_user uu ON wo.claim_uid = uu.uid
|
||||
$whereSql ORDER BY create_time DESC LIMIT ?,?";
|
||||
$param[] = $offset;
|
||||
$param[] = $size;
|
||||
$paramType .= 'ii';
|
||||
|
||||
$stmtList = mysqli_prepare($connWork, $listSql);
|
||||
mysqli_stmt_bind_param($stmtList, $paramType, ...$param);
|
||||
mysqli_stmt_execute($stmtList);
|
||||
$res = mysqli_stmt_get_result($stmtList);
|
||||
$list = [];
|
||||
while ($row = mysqli_fetch_assoc($res)) {
|
||||
$list[] = $row;
|
||||
}
|
||||
|
||||
// 总数统计
|
||||
$countSql = "SELECT COUNT(id) total FROM work_order $whereSql";
|
||||
$stmtCount = mysqli_prepare($connWork, $countSql);
|
||||
array_pop($param);
|
||||
array_pop($param);
|
||||
$paramTypeCount = rtrim($paramType, 'ii');
|
||||
if ($paramTypeCount) {
|
||||
mysqli_stmt_bind_param($stmtCount, $paramTypeCount, ...$param);
|
||||
}
|
||||
mysqli_stmt_execute($stmtCount);
|
||||
$countRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtCount));
|
||||
$total = intval($countRow['total']);
|
||||
|
||||
echo json_encode([
|
||||
'code' => 0,
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'size' => $size
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ===================== 根据告警ID查询关联工单 =====================
|
||||
if ($action === "get_workorder_by_alertid") {
|
||||
$alertId = trim($_GET['alert_id'] ?? '');
|
||||
if (empty($alertId)) {
|
||||
echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']);
|
||||
exit;
|
||||
}
|
||||
$alertEsc = mysqli_real_escape_string($connWork, $alertId);
|
||||
$sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC";
|
||||
$stmt = mysqli_prepare($connWork, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 's', $alertEsc);
|
||||
mysqli_stmt_execute($stmt);
|
||||
$res = mysqli_stmt_get_result($stmt);
|
||||
$list = [];
|
||||
while ($row = mysqli_fetch_assoc($res)) {
|
||||
$list[] = $row;
|
||||
}
|
||||
echo json_encode(['code' => 0, 'list' => $list]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ===================== 自动同步告警状态,批量更新工单为已完成 =====================
|
||||
if ($action === "auto_sync_workorder_status") {
|
||||
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
|
||||
$resolveRes = mysqli_query($connConf, $resolveSql);
|
||||
$resolveAlertIds = [];
|
||||
while ($row = mysqli_fetch_assoc($resolveRes)) {
|
||||
$resolveAlertIds[] = $row['alert_id'];
|
||||
}
|
||||
if (empty($resolveAlertIds)) {
|
||||
echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$inStr = implode("','", array_map(function($v) use ($connWork) {
|
||||
return mysqli_real_escape_string($connWork, $v);
|
||||
}, $resolveAlertIds));
|
||||
$updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)";
|
||||
mysqli_query($connWork, $updateSql);
|
||||
$updateCnt = mysqli_affected_rows($connWork);
|
||||
|
||||
echo json_encode([
|
||||
'code' => 0,
|
||||
'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成',
|
||||
'update_count' => $updateCnt
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
|
||||
if ($action === "get_firing_alert") {
|
||||
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
|
||||
$res = mysqli_query($connConf, $alertSql);
|
||||
$list = [];
|
||||
if($res instanceof mysqli_result){
|
||||
while ($row = mysqli_fetch_assoc($res)) {
|
||||
$list[] = $row;
|
||||
}
|
||||
}
|
||||
echo json_encode(['code' => 0, 'list' => $list]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2、新建工单
|
||||
if ($action === "create") {
|
||||
$title = trim($post['title'] ?? '');
|
||||
$content = trim($post['content'] ?? '');
|
||||
$assign = trim($post['assign_uid'] ?? '');
|
||||
$relateAlertId = trim($post['relate_alert_id'] ?? '');
|
||||
$notifyUser = trim($post['notify_user'] ?? '');
|
||||
$notifyChannel = trim($post['notify_channel'] ?? '');
|
||||
$status = 1;
|
||||
|
||||
if (!$title || !$content) {
|
||||
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!empty($relateAlertId)) {
|
||||
$alertEsc = mysqli_real_escape_string($connConf, $relateAlertId);
|
||||
$checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1";
|
||||
$stmtCheck = mysqli_prepare($connConf, $checkResolveSql);
|
||||
mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc);
|
||||
mysqli_stmt_execute($stmtCheck);
|
||||
$checkRes = mysqli_stmt_get_result($stmtCheck);
|
||||
if (mysqli_num_rows($checkRes) > 0) {
|
||||
$status = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// 新建工单默认无认领人 claim_uid=''
|
||||
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,is_read,claim_uid,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,?, '',NOW(),NOW())";
|
||||
$stmt = mysqli_prepare($connWork, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 'ssssssss', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
|
||||
$ok = mysqli_stmt_execute($stmt);
|
||||
if (!$ok) {
|
||||
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
|
||||
exit;
|
||||
}
|
||||
$orderId = mysqli_insert_id($connWork);
|
||||
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
|
||||
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========== 新增:工单认领接口 claim_work ==========
|
||||
if ($action === "claim_work") {
|
||||
$id = intval($post['id'] ?? 0);
|
||||
$uid = trim($post['uid'] ?? '');
|
||||
if ($id <= 0 || empty($uid)) {
|
||||
echo json_encode(["code" => 400, "msg" => "参数错误"]);
|
||||
exit;
|
||||
}
|
||||
// 校验工单未被认领
|
||||
$chkSql = "SELECT id FROM work_order WHERE id=? AND (claim_uid IS NULL OR claim_uid = '')";
|
||||
$stmtChk = mysqli_prepare($connWork, $chkSql);
|
||||
mysqli_stmt_bind_param($stmtChk, 'i', $id);
|
||||
mysqli_stmt_execute($stmtChk);
|
||||
$chkRes = mysqli_stmt_get_result($stmtChk);
|
||||
if(mysqli_num_rows($chkRes) === 0){
|
||||
echo json_encode(["code" => 400, "msg" => "该工单已被他人认领,无法重复认领"]);
|
||||
exit;
|
||||
}
|
||||
$updateSql = "UPDATE work_order SET claim_uid=?, claim_time=NOW() WHERE id=?";
|
||||
$stmt = mysqli_prepare($connWork, $updateSql);
|
||||
mysqli_stmt_bind_param($stmt, 'si', $uid, $id);
|
||||
$ok = mysqli_stmt_execute($stmt);
|
||||
if($ok){
|
||||
echo json_encode(["code" => 0, "msg" => "工单认领成功,现在你可以编辑处理"]);
|
||||
}else{
|
||||
echo json_encode(["code" => 500, "msg" => "认领失败:".mysqli_error($connWork)]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3、编辑工单(权限:管理员 或 当前用户是认领人)
|
||||
if ($action === "edit") {
|
||||
$id = intval($post['id'] ?? 0);
|
||||
$title = trim($post['title'] ?? '');
|
||||
$content = trim($post['content'] ?? '');
|
||||
$assign = trim($post['assign_uid'] ?? '');
|
||||
$relateAlertId = trim($post['relate_alert_id'] ?? '');
|
||||
$notifyUser = trim($post['notify_user'] ?? '');
|
||||
$notifyChannel = trim($post['notify_channel'] ?? '');
|
||||
if ($id <= 0 || empty($title) || empty($content)) {
|
||||
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
|
||||
exit;
|
||||
}
|
||||
// 权限重写
|
||||
if ($isAdmin !== 1) {
|
||||
$chkSql = "SELECT id FROM work_order WHERE id=? AND claim_uid=?";
|
||||
$stmtChk = mysqli_prepare($connWork, $chkSql);
|
||||
mysqli_stmt_bind_param($stmtChk, 'is', $id, $loginUid);
|
||||
mysqli_stmt_execute($stmtChk);
|
||||
$chkRes = mysqli_stmt_get_result($stmtChk);
|
||||
if (mysqli_num_rows($chkRes) === 0) {
|
||||
echo json_encode(['code' => 403, 'msg' => '仅工单认领人可编辑']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,relate_alert_id=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
|
||||
$stmt = mysqli_prepare($connWork, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 'ssssssi', $title, $content, $assign, $relateAlertId, $notifyUser, $notifyChannel, $id);
|
||||
mysqli_stmt_execute($stmt);
|
||||
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
|
||||
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4、修改工单状态(权限:管理员 或 当前用户是认领人)
|
||||
if ($action === "update_status") {
|
||||
$id = intval($post['id'] ?? 0);
|
||||
$status = intval($post['status'] ?? 1);
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
|
||||
exit;
|
||||
}
|
||||
if ($isAdmin !== 1) {
|
||||
$chkSql = "SELECT id FROM work_order WHERE id=? AND claim_uid=?";
|
||||
$stmtChk = mysqli_prepare($connWork, $chkSql);
|
||||
mysqli_stmt_bind_param($stmtChk, 'is', $id, $loginUid);
|
||||
mysqli_stmt_execute($stmtChk);
|
||||
$chkRes = mysqli_stmt_get_result($stmtChk);
|
||||
if (mysqli_num_rows($chkRes) === 0) {
|
||||
echo json_encode(['code' => 403, 'msg' => '仅工单认领人可修改状态']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$sql = "UPDATE work_order SET status=?,is_read=1,update_time=NOW() WHERE id=?";
|
||||
$stmt = mysqli_prepare($connWork, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
|
||||
mysqli_stmt_execute($stmt);
|
||||
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 5、管理员/拥有system_manage权限角色 分发工单
|
||||
if ($action === "assign") {
|
||||
if ($isAdmin !== 1 && !in_array("system_manage", $allowPages)) {
|
||||
echo json_encode(['code' => 403, 'msg' => '无工单分发权限,请联系管理员']);
|
||||
exit;
|
||||
}
|
||||
$id = intval($post['id'] ?? 0);
|
||||
$assignUid = trim($post['assign_uid'] ?? '');
|
||||
$sql = "UPDATE work_order SET assign_uid=?,is_read=0,update_time=NOW() WHERE id=?";
|
||||
$stmt = mysqli_prepare($connWork, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
|
||||
mysqli_stmt_execute($stmt);
|
||||
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 一键标记当前用户全部待处理工单为已读
|
||||
if ($action === 'mark_all_read') {
|
||||
$uid = $post['uid'] ?? '';
|
||||
$sql = "UPDATE work_order SET is_read=1 WHERE assign_uid=? AND status=1";
|
||||
$stmt = mysqli_prepare($connWork, $sql);
|
||||
mysqli_stmt_bind_param($stmt, "s", $uid);
|
||||
mysqli_stmt_execute($stmt);
|
||||
echo json_encode(['code' => 0, 'msg' => '已全部标记为已读']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 语言切换
|
||||
if ($action === 'save_lang') {
|
||||
$lang = $post['lang'] ?? 'zh';
|
||||
$sql = "UPDATE sys_config SET v=? WHERE k='lang'";
|
||||
$stmt = mysqli_prepare($connConf, $sql);
|
||||
mysqli_stmt_bind_param($stmt, "s", $lang);
|
||||
mysqli_stmt_execute($stmt);
|
||||
echo json_encode(['code' => 0, 'msg' => '成功']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 同步Prometheus实时告警,自动清理恢复告警
|
||||
if ($action === "sync_prom_alerts") {
|
||||
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
|
||||
$resp = @file_get_contents($promUrl);
|
||||
if($resp === false){
|
||||
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
$promData = json_decode($resp,true);
|
||||
if(!isset($promData['data']['alerts'])){
|
||||
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'");
|
||||
|
||||
$alerts = $promData['data']['alerts'] ?? [];
|
||||
foreach($alerts as $item){
|
||||
$alertName = $item['labels']['alertname'] ?? "unknown";
|
||||
$instance = $item['labels']['instance'] ?? "unknown";
|
||||
$alertId = $alertName."_".$instance;
|
||||
$sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()";
|
||||
$stmt = mysqli_prepare($connConf,$sql);
|
||||
mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance);
|
||||
mysqli_stmt_execute($stmt);
|
||||
}
|
||||
$syncNum = count($alerts);
|
||||
|
||||
$syncWorkUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . "?action=auto_sync_workorder_status";
|
||||
@file_get_contents($syncWorkUrl);
|
||||
|
||||
echo json_encode([
|
||||
"code"=>0,
|
||||
"msg"=>"Prometheus告警同步完成,已自动同步工单状态",
|
||||
"active_alert_total"=>$syncNum
|
||||
],JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 定时同步:告警恢复自动完工单
|
||||
if($action === "sync_alert_auto_close"){
|
||||
$alertApi = "./index.php?act=active_firing";
|
||||
$raw = @file_get_contents($alertApi);
|
||||
$activeList = json_decode($raw, true) ?: [];
|
||||
$activeKeys = [];
|
||||
foreach($activeList as $item){
|
||||
$activeKeys[] = $item['alert_name'] . "_" . $item['instance'];
|
||||
}
|
||||
$sql = "SELECT id, relate_alert_id FROM work_order WHERE status IN(1,2) AND relate_alert_id <> ''";
|
||||
$res = mysqli_query($connWork, $sql);
|
||||
while($row = mysqli_fetch_assoc($res)){
|
||||
if(!in_array($row['relate_alert_id'], $activeKeys)){
|
||||
$updateSql = "UPDATE work_order SET status=3, update_time=NOW() WHERE id = ".(int)$row['id'];
|
||||
mysqli_query($connWork, $updateSql);
|
||||
}
|
||||
}
|
||||
echo json_encode(['code'=>0, 'msg'=>'同步完成']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ==================== 全局通知统一函数 ====================
|
||||
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
|
||||
{
|
||||
if (empty($notifyUserStr) || empty($channelStr)) return;
|
||||
$userArr = explode(",", $notifyUserStr);
|
||||
$channelArr = explode(",", $channelStr);
|
||||
|
||||
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
|
||||
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
|
||||
if(empty($notifyCfg)) return;
|
||||
|
||||
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
|
||||
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
|
||||
$title = $order['title'];
|
||||
$content = $order['content'];
|
||||
$relateId = $order['relate_alert_id'] ?: "无";
|
||||
|
||||
$msgText = "【运维工单通知】\n工单ID:$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
|
||||
|
||||
foreach ($channelArr as $ch) {
|
||||
$ch = trim($ch);
|
||||
switch ($ch) {
|
||||
case "mail":
|
||||
sendMailNotify($notifyCfg, $connWork, $userArr, $title, $content, $relateId, $orderId);
|
||||
break;
|
||||
case "dingtalk":
|
||||
sendDingNotify($notifyCfg, $msgText);
|
||||
break;
|
||||
case "wecom":
|
||||
sendWecomNotify($notifyCfg, $msgText);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 邮件发送
|
||||
function sendMailNotify($smtp, $connWork, $uidList, $title, $content, $relateId, $orderId)
|
||||
{
|
||||
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
|
||||
$emailList = [];
|
||||
foreach ($uidList as $uid) {
|
||||
$uid = trim($uid);
|
||||
if (empty($uid)) continue;
|
||||
$res = mysqli_query($connWork, "SELECT email FROM sys_user WHERE uid='$uid'");
|
||||
$u = mysqli_fetch_assoc($res);
|
||||
if (!empty($u['email'])) $emailList[] = $u['email'];
|
||||
}
|
||||
if (empty($emailList)) return;
|
||||
$to = implode(",", array_unique($emailList));
|
||||
$subject = "【工单通知】#$orderId $title";
|
||||
$body = "<h3>工单 #$orderId</h3>
|
||||
<p>关联告警ID:$relateId</p>
|
||||
<p>标题:$title</p>
|
||||
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
|
||||
}
|
||||
|
||||
// 钉钉机器人推送
|
||||
function sendDingNotify($cfg, $text)
|
||||
{
|
||||
$webhook = $cfg['ding_webhook'] ?? '';
|
||||
$secret = $cfg['ding_secret'] ?? '';
|
||||
if(empty($webhook)) return;
|
||||
$data = json_encode([
|
||||
"msgtype" => "text",
|
||||
"text" => ["content" => $text]
|
||||
]);
|
||||
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
|
||||
file_get_contents($webhook, false, stream_context_create($opts));
|
||||
}
|
||||
|
||||
// 企业微信机器人推送
|
||||
function sendWecomNotify($cfg, $text)
|
||||
{
|
||||
$webhook = $cfg['wecom_webhook'] ?? '';
|
||||
if(empty($webhook)) return;
|
||||
$data = json_encode([
|
||||
"msgtype" => "text",
|
||||
"text" => ["content" => $text]
|
||||
]);
|
||||
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
|
||||
file_get_contents($webhook, false, stream_context_create($opts));
|
||||
}
|
||||
|
||||
// 无匹配动作
|
||||
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
|
||||
mysqli_close($connWork);
|
||||
mysqli_close($connConf);
|
||||
exit;
|
||||
?>
|
||||
@@ -9,7 +9,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
}
|
||||
session_start();
|
||||
$expire = 1800;
|
||||
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
|
||||
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time']) > $expire) {
|
||||
echo json_encode(['code' => 401, 'msg' => '登录失效']);
|
||||
exit;
|
||||
}
|
||||
@@ -42,7 +42,8 @@ mysqli_set_charset($connConf, "utf8mb4");
|
||||
// 获取当前用户角色权限
|
||||
$isAdmin = 0;
|
||||
$permList = "";
|
||||
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
|
||||
$roleId = 0;
|
||||
$roleSql = "SELECT r.is_admin, r.perm_list, u.role_id FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
|
||||
$stmtRole = mysqli_prepare($connWork, $roleSql);
|
||||
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
|
||||
mysqli_stmt_execute($stmtRole);
|
||||
@@ -51,12 +52,98 @@ $roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
|
||||
if ($roleRow) {
|
||||
$isAdmin = intval($roleRow['is_admin']);
|
||||
$permList = $roleRow['perm_list'];
|
||||
$roleId = intval($roleRow['role_id'] ?? 0);
|
||||
}
|
||||
|
||||
// 读取当前角色页面权限列表(用于分发工单权限判断)
|
||||
$allowPages = [];
|
||||
if ($roleId > 0) {
|
||||
$permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = ?";
|
||||
$stmtPerm = mysqli_prepare($connWork, $permSql);
|
||||
mysqli_stmt_bind_param($stmtPerm, 'i', $roleId);
|
||||
mysqli_stmt_execute($stmtPerm);
|
||||
$pRes = mysqli_stmt_get_result($stmtPerm);
|
||||
while ($p = mysqli_fetch_assoc($pRes)) {
|
||||
$allowPages[] = $p['page_key'];
|
||||
}
|
||||
}
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$post = json_decode(file_get_contents("php://input"), true) ?: [];
|
||||
|
||||
// ===================== 新增1:根据告警ID查询关联工单 =====================
|
||||
// ===================== 工单列表查询(新增,解决数据可见权限隔离) =====================
|
||||
if ($action === "get_list") {
|
||||
$page = intval($_GET['page'] ?? 1);
|
||||
$size = intval($_GET['size'] ?? 20);
|
||||
$offset = ($page - 1) * $size;
|
||||
$status = trim($_GET['status'] ?? '');
|
||||
$keyword = trim($_GET['keyword'] ?? '');
|
||||
|
||||
$where = [];
|
||||
$param = [];
|
||||
$paramType = '';
|
||||
|
||||
// 普通用户:仅能查看自己创建 或 分配给自己的工单;管理员无限制查看全部
|
||||
if ($isAdmin !== 1) {
|
||||
$where[] = "(create_uid = ? OR assign_uid = ?)";
|
||||
$param[] = $loginUid;
|
||||
$param[] = $loginUid;
|
||||
$paramType .= 'ss';
|
||||
}
|
||||
|
||||
if ($status !== '') {
|
||||
$where[] = "status = ?";
|
||||
$param[] = $status;
|
||||
$paramType .= 'i';
|
||||
}
|
||||
if ($keyword !== '') {
|
||||
$where[] = "(title LIKE ? OR content LIKE ?)";
|
||||
$param[] = "%$keyword%";
|
||||
$param[] = "%$keyword%";
|
||||
$paramType .= 'ss';
|
||||
}
|
||||
|
||||
$whereSql = $where ? "WHERE " . implode(" AND ", $where) : "";
|
||||
|
||||
// 分页列表
|
||||
$listSql = "SELECT * FROM work_order $whereSql ORDER BY create_time DESC LIMIT ?,?";
|
||||
$param[] = $offset;
|
||||
$param[] = $size;
|
||||
$paramType .= 'ii';
|
||||
|
||||
$stmtList = mysqli_prepare($connWork, $listSql);
|
||||
mysqli_stmt_bind_param($stmtList, $paramType, ...$param);
|
||||
mysqli_stmt_execute($stmtList);
|
||||
$res = mysqli_stmt_get_result($stmtList);
|
||||
$list = [];
|
||||
while ($row = mysqli_fetch_assoc($res)) {
|
||||
$list[] = $row;
|
||||
}
|
||||
|
||||
// 总数统计
|
||||
$countSql = "SELECT COUNT(id) total FROM work_order $whereSql";
|
||||
$stmtCount = mysqli_prepare($connWork, $countSql);
|
||||
array_pop($param);
|
||||
array_pop($param);
|
||||
$paramTypeCount = rtrim($paramType, 'ii');
|
||||
if ($paramTypeCount) {
|
||||
mysqli_stmt_bind_param($stmtCount, $paramTypeCount, ...$param);
|
||||
}
|
||||
mysqli_stmt_execute($stmtCount);
|
||||
$countRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtCount));
|
||||
$total = intval($countRow['total']);
|
||||
|
||||
echo json_encode([
|
||||
'code' => 0,
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'size' => $size
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ===================== 根据告警ID查询关联工单 =====================
|
||||
if ($action === "get_workorder_by_alertid") {
|
||||
$alertId = trim($_GET['alert_id'] ?? '');
|
||||
if (empty($alertId)) {
|
||||
@@ -77,7 +164,7 @@ if ($action === "get_workorder_by_alertid") {
|
||||
exit;
|
||||
}
|
||||
|
||||
// ===================== 新增2:自动同步告警状态,批量更新工单为已完成 =====================
|
||||
// ===================== 自动同步告警状态,批量更新工单为已完成 =====================
|
||||
if ($action === "auto_sync_workorder_status") {
|
||||
// 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID
|
||||
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
|
||||
@@ -228,10 +315,11 @@ if ($action === "update_status") {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 5、管理员分发工单(前端统一action=assign,废弃assign_order)
|
||||
// 5、管理员/拥有system_manage权限角色 分发工单
|
||||
if ($action === "assign") {
|
||||
if ($isAdmin !== 1) {
|
||||
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
|
||||
// 修复:超级管理员 或 拥有system_manage页面权限均可分发工单
|
||||
if ($isAdmin !== 1 && !in_array("system_manage", $allowPages)) {
|
||||
echo json_encode(['code' => 403, 'msg' => '无工单分发权限,请联系管理员']);
|
||||
exit;
|
||||
}
|
||||
$id = intval($post['id'] ?? 0);
|
||||
@@ -267,7 +355,7 @@ if ($action === 'save_lang') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 新增:同步Prometheus实时告警,自动清理恢复告警(完全独立,无需index.php)
|
||||
// 同步Prometheus实时告警,自动清理恢复告警
|
||||
if ($action === "sync_prom_alerts") {
|
||||
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
|
||||
$resp = @file_get_contents($promUrl);
|
||||
@@ -26,7 +26,7 @@ $userName = htmlspecialchars($_SESSION['username'] ?? '');
|
||||
$isAdmin = intval($_SESSION['is_admin'] ?? 0);
|
||||
$userRoleId = intval($_SESSION['role_id'] ?? 0);
|
||||
|
||||
// ====================== 双库固定连接(修复null根源) ======================
|
||||
// ====================== 双库固定连接 ======================
|
||||
$dbHost = "10.150.117.190";
|
||||
$dbUser = "root";
|
||||
$dbPass = "hp93000";
|
||||
@@ -48,27 +48,43 @@ if (!$connMonitor) {
|
||||
mysqli_set_charset($connMonitor, "utf8mb4");
|
||||
// ========================================================================
|
||||
|
||||
// 获取当前访问页面文件名(去掉.php后缀)
|
||||
$currentPage = basename($_SERVER['SCRIPT_NAME'], '.php');
|
||||
// ========== 新增:读取角色真实名称 ==========
|
||||
$roleRealName = "未知角色";
|
||||
if ($userRoleId > 0) {
|
||||
$getRoleSql = "SELECT role_name FROM sys_role WHERE id = $userRoleId";
|
||||
$roleResult = mysqli_query($connMonitor, $getRoleSql);
|
||||
if ($roleResult && $roleRow = mysqli_fetch_assoc($roleResult)) {
|
||||
$roleRealName = $roleRow['role_name'];
|
||||
}
|
||||
}
|
||||
// 超级管理员强制覆盖文字
|
||||
if ($isAdmin === 1) {
|
||||
$roleRealName = "超级管理员";
|
||||
}
|
||||
// ===========================================
|
||||
|
||||
// 非超级管理员校验页面权限
|
||||
if ($isAdmin !== 1) {
|
||||
// 1. 登录时一次性加载当前角色全部权限,存入全局变量(侧边栏共用)
|
||||
$allowPages = [];
|
||||
if ($userRoleId > 0) {
|
||||
$permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = $userRoleId";
|
||||
$permRes = mysqli_query($connMonitor, $permSql);
|
||||
$allowPages = [];
|
||||
if ($permRes) {
|
||||
while ($row = mysqli_fetch_assoc($permRes)) {
|
||||
$allowPages[] = $row['page_key'];
|
||||
}
|
||||
}
|
||||
// 无权限拦截
|
||||
if (!in_array($currentPage, $allowPages)) {
|
||||
echo "<script>alert('无访问权限!');history.back();</script>";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取右上角未读消息数量(修复第79/84/86行报错)
|
||||
// 2. 获取当前访问页面文件名(去掉.php后缀)
|
||||
$currentPage = basename($_SERVER['SCRIPT_NAME'], '.php');
|
||||
|
||||
// 3. 非管理员自动拦截无权限页面(全局统一校验,无需每个页面重复写)
|
||||
if ($isAdmin !== 1 && !in_array($currentPage, $allowPages)) {
|
||||
echo "<script>alert('无访问权限!');history.back();</script>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// 读取右上角未读消息数量
|
||||
$unreadMsgCount = 0;
|
||||
$msgSql = "SELECT COUNT(id) cnt FROM sys_user_msg WHERE user_id = $loginUserId AND is_read = 0";
|
||||
$msgRes = mysqli_query($connMonitor, $msgSql);
|
||||
@@ -142,4 +158,6 @@ if (!empty($sidebarRawLinks)) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 【已删除冲突无用函数 hasPagePerm】
|
||||
?>
|
||||
@@ -1,8 +0,0 @@
|
||||
modules:
|
||||
tcp_connect:
|
||||
prober: tcp
|
||||
timeout: 5s
|
||||
icmp:
|
||||
prober: icmp
|
||||
icmp:
|
||||
preferred_ip_protocol: ip4
|
||||
@@ -6,6 +6,9 @@ header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
|
||||
|
||||
|
||||
require_once 'auth.php';
|
||||
// 权限拦截
|
||||
// 会话超时:30分钟无操作自动登出,与admin后台统一
|
||||
$expire = 1800;
|
||||
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
|
||||
@@ -51,6 +54,7 @@ $langMap = [
|
||||
'monitor_panel' => '监控面板',
|
||||
'alert_overview' => '告警监控总览',
|
||||
'disk_capacity' => '服务器磁盘容量',
|
||||
'docker_mgr' => 'Docker容器管理',
|
||||
'alert_list_page' => '历史告警查询',
|
||||
'work_order_mgr' => '工单系统',
|
||||
'system_manager' => '角色管理',
|
||||
@@ -124,6 +128,7 @@ $langMap = [
|
||||
'monitor_panel' => 'Monitor Panel',
|
||||
'alert_overview' => 'Alert Overview',
|
||||
'disk_capacity' => 'Server Disk Capacity',
|
||||
'docker_mgr' => 'Docker Manager',
|
||||
'alert_list_page' => 'Alert Pagination List',
|
||||
'work_order_mgr' => 'Work Order System',
|
||||
'system_manager' => 'Role & Permission Manage',
|
||||
@@ -636,24 +641,44 @@ tbody tr:nth-child(even){background:#fbfcfe}
|
||||
</div>
|
||||
<div class="sidebar-menu">
|
||||
<div class="menu-title"><?php echo $t['monitor_panel']; ?></div>
|
||||
|
||||
<?php if ($isAdmin === 1 || in_array("dashboard", $allowPages)): ?>
|
||||
<div class="menu-item active" data-page="alertPage">
|
||||
<i class="fa fa-line-chart"></i>
|
||||
<span><?php echo $t['alert_overview']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isAdmin === 1 || in_array("disk", $allowPages)): ?>
|
||||
<div class="menu-item" data-page="diskPage">
|
||||
<i class="fa fa-hdd-o"></i>
|
||||
<span><?php echo $t['disk_capacity']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isAdmin === 1 || in_array("docker_mgr", $allowPages)): ?>
|
||||
<div class="menu-item" onclick="location.href='docker_mgr.php'">
|
||||
<i class="fa fa-cubes"></i>
|
||||
<span><?php echo $t['docker_mgr']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 告警分页:去掉window.open,同页跳转 -->
|
||||
<?php if ($isAdmin === 1 || in_array("history_query", $allowPages)): ?>
|
||||
<div class="menu-item" onclick="location.href='alert_list.php'">
|
||||
<i class="fa fa-list"></i>
|
||||
<span><?php echo $t['alert_list_page']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 工单 图标 fa-ticket -->
|
||||
<?php if ($isAdmin === 1 || in_array("work_order", $allowPages)): ?>
|
||||
<div class="menu-item" onclick="location.href='work_order.php'" data-page="workOrderPage">
|
||||
<i class="fa fa-ticket"></i>
|
||||
<span><?php echo $t['work_order_mgr']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 后台配置快捷外链自动渲染 -->
|
||||
<?php if (!empty($sidebarLinkList)): ?>
|
||||
<div class="menu-title"><?php echo $t['quick_link']; ?></div>
|
||||
@@ -664,16 +689,23 @@ tbody tr:nth-child(even){background:#fbfcfe}
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="menu-title"><?php echo $t['system_manage']; ?></div>
|
||||
|
||||
<?php if ($isAdmin === 1 || in_array("smtp_config", $allowPages)): ?>
|
||||
<div class="menu-item" onclick="location.href='admin.php'">
|
||||
<i class="fa fa-cog"></i>
|
||||
<span><?php echo $t['system_config']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 新增角色管理菜单 -->
|
||||
<?php if ($isAdmin === 1 || in_array("system_manage", $allowPages)): ?>
|
||||
<div class="menu-item" onclick="location.href='system_manage.php'">
|
||||
<i class="fa fa-users"></i>
|
||||
<span><?php echo $t['system_manager']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧主内容区 -->
|
||||
@@ -686,7 +718,7 @@ tbody tr:nth-child(even){background:#fbfcfe}
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleText; ?>)</span>
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleRealName; ?>)</span>
|
||||
<span class="time-info"><i class="fa fa-clock-o"></i><?php echo $t['sys_time']; ?><span id="updateTime">--</span></span>
|
||||
<!-- 语言切换下拉框 -->
|
||||
<div class="lang-box">
|
||||
+1175
File diff suppressed because it is too large
Load Diff
+1193
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user