Compare commits

1 Commits

Author SHA1 Message Date
admin 0bb2eb4c36 更新 2026-07-09 16:31:30 +08:00
169 changed files with 8823 additions and 5390 deletions
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
-20
View File
@@ -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端口,不再用wgetwget不兼容fastcgi
command: ["/bin/sh", "-c", "until nc -z php 9000; do echo '等待php-fpm端口就绪...'; sleep 1; done; nginx -g 'daemon off;'"]
-23
View File
@@ -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;'"]
-17
View File
@@ -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
-54
View File
@@ -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;
}
}
-25
View File
@@ -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/;
}
}
-53
View File
@@ -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;
}
}
-45
View File
@@ -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;
}
}
-59
View File
@@ -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;
}
}
-19
View File
@@ -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;
}
}
-150
View File
@@ -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()
-77
View File
@@ -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))
-205
View File
@@ -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] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:34:17] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:34:17] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:34:17] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:35:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:35:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:35:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:35:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:38] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:42] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:42] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:48] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:11] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:11] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:11] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:18] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:46] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:58:57] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:59:12] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:59:12] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:59:12] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 17:28:04] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 19:23:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 19:24:03] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [03/Jul/2026 12:30:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [03/Jul/2026 12:30:38] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [03/Jul/2026 15:19:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [03/Jul/2026 15:19:53] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 13:44:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 13:50:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 13:56:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 13:57:03] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 14:26:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 14:28:53] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 14:33:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 14:33:38] "POST /alert HTTP/1.1" 200 -
10.150.44.16 - - [06/Jul/2026 09:36:01] "GET / HTTP/1.1" 404 -
10.150.44.16 - - [06/Jul/2026 09:36:02] "GET /favicon.ico HTTP/1.1" 404 -
10.150.117.190 - - [06/Jul/2026 11:21:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:21:31] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:25:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:28:03] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:28:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:30:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:30:31] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:31:46] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 12:21:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 13:54:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 13:55:18] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:44:18] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:45:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:45:26] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:45:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:50:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:50:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:52:38] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:52:38] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:54:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:54:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:54:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:54:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:13:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:13:33] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:16:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:16:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:18:48] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:18:48] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:49:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:50:48] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 16:00:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 16:01:08] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 17:22:05] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 09:42:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 09:43:38] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 09:46:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 09:46:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:11:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:11:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:13:08] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:13:08] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:23:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:24:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:25:18] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:29:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:50:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:21:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:24:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:24:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:37:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:37:31] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:43:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:44:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:44:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:45:03] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:46:03] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 12:12:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 12:13:48] "POST /alert HTTP/1.1" 200 -
-65
View File
@@ -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)
-47
View File
@@ -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)
-47
View File
@@ -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)
-72
View File
@@ -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)
-55
View File
@@ -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(); session_start();
header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache"); header("Pragma: no-cache");
require_once 'auth.php';
$expire = 1800; $expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) { if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy(); session_destroy();
@@ -80,6 +81,7 @@ td{padding:12px 10px;border-bottom:1px solid #F2F3F5;color:#1D2129;}
<div class="nav-right"> <div class="nav-right">
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a> <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 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> <button class="btn btn-danger" id="logoutBtn"><i class="fa fa-sign-out"></i> 退出登录</button>
</div> </div>
</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
View File
@@ -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;
+752
View File
@@ -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"
]
# ===================== 工具函数1docker 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))
# ===================== 工具函数3HTTP 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
# ===================== 新增工具函数4WebSocket连接专用鉴权函数 =====================
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-pyexec_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)
+100
View File
@@ -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)
+669
View File
@@ -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"
]
# ===================== 工具函数1docker 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))
# ===================== 工具函数3HMAC签名鉴权,防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-pyexec_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)
+9
View File
@@ -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头) // CSV导出接口(支持指定历史日期下载,不输出JSON头)
if ($act === "export_csv") { if ($act === "export_csv") {
date_default_timezone_set('Asia/Shanghai');
$conn = getDbConn(); $conn = getDbConn();
mysqli_query($conn, "SET time_zone = '+08:00'");
if (!$conn) { if (!$conn) {
header("Content-Type:text/html;charset=utf-8"); header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error(); echo "数据库连接失败:" . mysqli_connect_error();
exit; exit;
} }
// 接收自定义日期参数,不传则取今日
$targetDate = $_GET['date'] ?? date("Y-m-d"); $singleDate = $_GET['date'] ?? '';
// 简单日期格式校验 Y-m-d $startDate = $_GET['start'] ?? '';
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $targetDate)) { $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"); 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; 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 $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); $res = mysqli_query($conn, $sql);
ob_clean(); ob_clean();
header("Content-Type: text/csv; charset=utf-8"); 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"; echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"]; $header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n"; echo implode(",", $header) . "\r\n";
@@ -69,7 +99,6 @@ if ($act === "export_csv") {
mysqli_close($conn); mysqli_close($conn);
exit; exit;
} }
// 所有JSON接口统一返回头 // 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8"); header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn(); $conn = getDbConn();
@@ -322,13 +322,25 @@ button.normal:hover{
<!-- 9 导出CSV报表(带日历选择) --> <!-- 9 导出CSV报表(带日历选择) -->
<div class="card"> <div class="card">
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表(支持历史日期)</h3> <h3><i class="fa fa-download"></i>9. 导出告警CSV报表</h3>
<div class="row">
<label>选择日期:</label> <!-- 第一组:单日导出(原有功能保留) -->
<div class="row" style="margin-bottom:12px;gap:10px;align-items:center;flex-wrap:wrap;">
<label>单日导出:</label>
<input type="date" id="csv_date" placeholder="留空导出今日"> <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>
<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> </div>
<!-- 返回结果输出 --> <!-- 返回结果输出 -->
@@ -397,8 +409,8 @@ function simpleReq(act){
}) })
} }
// 导出CSV 支持日历选择日期 // 单日导出
function exportCsv(){ function exportCsvSingle(){
const dateVal = document.getElementById("csv_date").value.trim(); const dateVal = document.getElementById("csv_date").value.trim();
let url = `${apiUrl}?act=export_csv`; let url = `${apiUrl}?act=export_csv`;
if(dateVal){ if(dateVal){
@@ -406,6 +418,23 @@ function exportCsv(){
} }
window.open(url,"_blank"); 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> </script>
</body> </body>
</html> </html>
+528
View File
@@ -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(); session_start();
$expire = 1800; $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' => '登录失效']); echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit; exit;
} }
@@ -42,7 +42,8 @@ mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限 // 获取当前用户角色权限
$isAdmin = 0; $isAdmin = 0;
$permList = ""; $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); $stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid); mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole); mysqli_stmt_execute($stmtRole);
@@ -51,12 +52,98 @@ $roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) { if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']); $isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list']; $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'] ?? ''; $action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: []; $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") { if ($action === "get_workorder_by_alertid") {
$alertId = trim($_GET['alert_id'] ?? ''); $alertId = trim($_GET['alert_id'] ?? '');
if (empty($alertId)) { if (empty($alertId)) {
@@ -77,7 +164,7 @@ if ($action === "get_workorder_by_alertid") {
exit; exit;
} }
// ===================== 新增2自动同步告警状态,批量更新工单为已完成 ===================== // ===================== 自动同步告警状态,批量更新工单为已完成 =====================
if ($action === "auto_sync_workorder_status") { if ($action === "auto_sync_workorder_status") {
// 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID // 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2"; $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; exit;
} }
// 5、管理员分发工单(前端统一action=assign,废弃assign_order // 5、管理员/拥有system_manage权限角色 分发工单
if ($action === "assign") { if ($action === "assign") {
if ($isAdmin !== 1) { // 修复:超级管理员 或 拥有system_manage页面权限均可分发工单
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']); if ($isAdmin !== 1 && !in_array("system_manage", $allowPages)) {
echo json_encode(['code' => 403, 'msg' => '无工单分发权限,请联系管理员']);
exit; exit;
} }
$id = intval($post['id'] ?? 0); $id = intval($post['id'] ?? 0);
@@ -267,7 +355,7 @@ if ($action === 'save_lang') {
exit; exit;
} }
// 新增:同步Prometheus实时告警,自动清理恢复告警(完全独立,无需index.php // 同步Prometheus实时告警,自动清理恢复告警
if ($action === "sync_prom_alerts") { if ($action === "sync_prom_alerts") {
$promUrl = "http://10.150.117.190:9090/api/v1/alerts"; $promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl); $resp = @file_get_contents($promUrl);
+30 -12
View File
@@ -26,7 +26,7 @@ $userName = htmlspecialchars($_SESSION['username'] ?? '');
$isAdmin = intval($_SESSION['is_admin'] ?? 0); $isAdmin = intval($_SESSION['is_admin'] ?? 0);
$userRoleId = intval($_SESSION['role_id'] ?? 0); $userRoleId = intval($_SESSION['role_id'] ?? 0);
// ====================== 双库固定连接(修复null根源) ====================== // ====================== 双库固定连接 ======================
$dbHost = "10.150.117.190"; $dbHost = "10.150.117.190";
$dbUser = "root"; $dbUser = "root";
$dbPass = "hp93000"; $dbPass = "hp93000";
@@ -48,27 +48,43 @@ if (!$connMonitor) {
mysqli_set_charset($connMonitor, "utf8mb4"); 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 = "超级管理员";
}
// ===========================================
// 非超级管理员校验页面权限 // 1. 登录时一次性加载当前角色全部权限,存入全局变量(侧边栏共用)
if ($isAdmin !== 1) { $allowPages = [];
if ($userRoleId > 0) {
$permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = $userRoleId"; $permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = $userRoleId";
$permRes = mysqli_query($connMonitor, $permSql); $permRes = mysqli_query($connMonitor, $permSql);
$allowPages = [];
if ($permRes) { if ($permRes) {
while ($row = mysqli_fetch_assoc($permRes)) { while ($row = mysqli_fetch_assoc($permRes)) {
$allowPages[] = $row['page_key']; $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; $unreadMsgCount = 0;
$msgSql = "SELECT COUNT(id) cnt FROM sys_user_msg WHERE user_id = $loginUserId AND is_read = 0"; $msgSql = "SELECT COUNT(id) cnt FROM sys_user_msg WHERE user_id = $loginUserId AND is_read = 0";
$msgRes = mysqli_query($connMonitor, $msgSql); $msgRes = mysqli_query($connMonitor, $msgSql);
@@ -142,4 +158,6 @@ if (!empty($sidebarRawLinks)) {
} }
} }
} }
// 【已删除冲突无用函数 hasPagePerm】
?> ?>
-8
View File
@@ -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("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT"); header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
require_once 'auth.php';
// 权限拦截
// 会话超时:30分钟无操作自动登出,与admin后台统一 // 会话超时:30分钟无操作自动登出,与admin后台统一
$expire = 1800; $expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) { if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
@@ -51,6 +54,7 @@ $langMap = [
'monitor_panel' => '监控面板', 'monitor_panel' => '监控面板',
'alert_overview' => '告警监控总览', 'alert_overview' => '告警监控总览',
'disk_capacity' => '服务器磁盘容量', 'disk_capacity' => '服务器磁盘容量',
'docker_mgr' => 'Docker容器管理',
'alert_list_page' => '历史告警查询', 'alert_list_page' => '历史告警查询',
'work_order_mgr' => '工单系统', 'work_order_mgr' => '工单系统',
'system_manager' => '角色管理', 'system_manager' => '角色管理',
@@ -124,6 +128,7 @@ $langMap = [
'monitor_panel' => 'Monitor Panel', 'monitor_panel' => 'Monitor Panel',
'alert_overview' => 'Alert Overview', 'alert_overview' => 'Alert Overview',
'disk_capacity' => 'Server Disk Capacity', 'disk_capacity' => 'Server Disk Capacity',
'docker_mgr' => 'Docker Manager',
'alert_list_page' => 'Alert Pagination List', 'alert_list_page' => 'Alert Pagination List',
'work_order_mgr' => 'Work Order System', 'work_order_mgr' => 'Work Order System',
'system_manager' => 'Role & Permission Manage', 'system_manager' => 'Role & Permission Manage',
@@ -636,24 +641,44 @@ tbody tr:nth-child(even){background:#fbfcfe}
</div> </div>
<div class="sidebar-menu"> <div class="sidebar-menu">
<div class="menu-title"><?php echo $t['monitor_panel']; ?></div> <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"> <div class="menu-item active" data-page="alertPage">
<i class="fa fa-line-chart"></i> <i class="fa fa-line-chart"></i>
<span><?php echo $t['alert_overview']; ?></span> <span><?php echo $t['alert_overview']; ?></span>
</div> </div>
<?php endif; ?>
<?php if ($isAdmin === 1 || in_array("disk", $allowPages)): ?>
<div class="menu-item" data-page="diskPage"> <div class="menu-item" data-page="diskPage">
<i class="fa fa-hdd-o"></i> <i class="fa fa-hdd-o"></i>
<span><?php echo $t['disk_capacity']; ?></span> <span><?php echo $t['disk_capacity']; ?></span>
</div> </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,同页跳转 --> <!-- 告警分页:去掉window.open,同页跳转 -->
<?php if ($isAdmin === 1 || in_array("history_query", $allowPages)): ?>
<div class="menu-item" onclick="location.href='alert_list.php'"> <div class="menu-item" onclick="location.href='alert_list.php'">
<i class="fa fa-list"></i> <i class="fa fa-list"></i>
<span><?php echo $t['alert_list_page']; ?></span> <span><?php echo $t['alert_list_page']; ?></span>
</div> </div>
<?php endif; ?>
<!-- 工单 图标 fa-ticket --> <!-- 工单 图标 fa-ticket -->
<?php if ($isAdmin === 1 || in_array("work_order", $allowPages)): ?>
<div class="menu-item" onclick="location.href='work_order.php'" data-page="workOrderPage"> <div class="menu-item" onclick="location.href='work_order.php'" data-page="workOrderPage">
<i class="fa fa-ticket"></i> <i class="fa fa-ticket"></i>
<span><?php echo $t['work_order_mgr']; ?></span> <span><?php echo $t['work_order_mgr']; ?></span>
</div> </div>
<?php endif; ?>
<!-- 后台配置快捷外链自动渲染 --> <!-- 后台配置快捷外链自动渲染 -->
<?php if (!empty($sidebarLinkList)): ?> <?php if (!empty($sidebarLinkList)): ?>
<div class="menu-title"><?php echo $t['quick_link']; ?></div> <div class="menu-title"><?php echo $t['quick_link']; ?></div>
@@ -664,16 +689,23 @@ tbody tr:nth-child(even){background:#fbfcfe}
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>
<div class="menu-title"><?php echo $t['system_manage']; ?></div> <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'"> <div class="menu-item" onclick="location.href='admin.php'">
<i class="fa fa-cog"></i> <i class="fa fa-cog"></i>
<span><?php echo $t['system_config']; ?></span> <span><?php echo $t['system_config']; ?></span>
</div> </div>
<?php endif; ?>
<!-- 新增角色管理菜单 --> <!-- 新增角色管理菜单 -->
<?php if ($isAdmin === 1 || in_array("system_manage", $allowPages)): ?>
<div class="menu-item" onclick="location.href='system_manage.php'"> <div class="menu-item" onclick="location.href='system_manage.php'">
<i class="fa fa-users"></i> <i class="fa fa-users"></i>
<span><?php echo $t['system_manager']; ?></span> <span><?php echo $t['system_manager']; ?></span>
</div> </div>
<?php endif; ?>
</div> </div>
</div> </div>
<!-- 右侧主内容区 --> <!-- 右侧主内容区 -->
@@ -686,7 +718,7 @@ tbody tr:nth-child(even){background:#fbfcfe}
</div> </div>
</div> </div>
<div class="header-right"> <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> <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"> <div class="lang-box">
+1175
View File
File diff suppressed because it is too large Load Diff
+1193
View File
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