- P0: 实例访问控制全线接入权限组 (829测试) - P1: permissions字段激活 + 操作级权限检查 (843测试) - 前端: 删除数据权限页面, 权限组合并为唯一入口 - 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析 - 新增: MongoDB管理页面 + Dashboard增强 - 修复: Pydantic V2迁移 + 前端TS错误修复
353 lines
12 KiB
Python
353 lines
12 KiB
Python
"""MySQL instance detail management API — databases, tables, users, slow queries, processlist, status, variables."""
|
|
import asyncio
|
|
from typing import Optional, List
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import require_role
|
|
from app.core.permissions import check_instance_access
|
|
from app.models.user import User
|
|
from app.models.instance import Instance
|
|
from app.core.crypto import decrypt_password
|
|
|
|
router = APIRouter(prefix="/instances", tags=["实例详情"])
|
|
|
|
|
|
def _get_instance_or_404(db: AsyncSession, instance_id: int):
|
|
"""Helper to get instance or raise 404. Returns coroutine."""
|
|
return db.execute(select(Instance).where(Instance.id == instance_id))
|
|
|
|
|
|
async def _get_decrypted_instance(db: AsyncSession, instance_id: int) -> Instance:
|
|
result = await _get_instance_or_404(db, instance_id)
|
|
instance = result.scalar_one_or_none()
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail="数据源不存在")
|
|
instance.password = decrypt_password(instance.password)
|
|
return instance
|
|
|
|
|
|
async def _mysql_query(instance: Instance, sql: str, database: Optional[str] = None) -> list:
|
|
"""Execute a MySQL query and return rows as list of dicts."""
|
|
import aiomysql
|
|
conn = await aiomysql.connect(
|
|
host=instance.host, port=instance.port,
|
|
user=instance.username, password=instance.password,
|
|
db=database or instance.database,
|
|
)
|
|
try:
|
|
async with conn.cursor(aiomysql.DictCursor) as cur:
|
|
await cur.execute(sql)
|
|
rows = await cur.fetchall()
|
|
return rows
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
async def _mysql_execute(instance: Instance, sql: str, database: Optional[str] = None):
|
|
"""Execute a MySQL statement (no result set needed)."""
|
|
import aiomysql
|
|
conn = await aiomysql.connect(
|
|
host=instance.host, port=instance.port,
|
|
user=instance.username, password=instance.password,
|
|
db=database or instance.database,
|
|
)
|
|
try:
|
|
async with conn.cursor() as cur:
|
|
await cur.execute(sql)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# ---- Databases ----
|
|
|
|
@router.get("/{instance_id}/detail/databases")
|
|
async def list_databases(
|
|
instance_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin", "dba")),
|
|
):
|
|
instance = await _get_decrypted_instance(db, instance_id)
|
|
# P0: 权限检查
|
|
await check_instance_access(current_user, instance, db)
|
|
if instance.db_type != "mysql":
|
|
raise HTTPException(status_code=400, detail="仅支持MySQL实例")
|
|
try:
|
|
rows = await _mysql_query(instance, "SHOW DATABASES")
|
|
databases = [
|
|
row["Database"] for row in rows
|
|
if row["Database"] not in ("information_schema", "performance_schema", "sys", "mysql")
|
|
]
|
|
return databases
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail="查询数据库列表失败: " + str(e))
|
|
|
|
|
|
# ---- Tables ----
|
|
|
|
@router.get("/{instance_id}/detail/tables")
|
|
async def list_tables(
|
|
instance_id: int,
|
|
database: str = Query(..., description="数据库名"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin", "dba")),
|
|
):
|
|
instance = await _get_decrypted_instance(db, instance_id)
|
|
# P0: 权限检查
|
|
await check_instance_access(current_user, instance, db)
|
|
if instance.db_type != "mysql":
|
|
raise HTTPException(status_code=400, detail="仅支持MySQL实例")
|
|
try:
|
|
rows = await _mysql_query(instance, "SHOW TABLE STATUS", database=database)
|
|
tables = []
|
|
for row in rows:
|
|
tables.append({
|
|
"name": row.get("Name", ""),
|
|
"engine": row.get("Engine", ""),
|
|
"rows": row.get("Rows", 0),
|
|
"data_length": row.get("Data_length", 0),
|
|
"index_length": row.get("Index_length", 0),
|
|
"comment": row.get("Comment", ""),
|
|
"collation": row.get("Collation", ""),
|
|
"create_time": str(row.get("Create_time", "")),
|
|
"update_time": str(row.get("Update_time", "")),
|
|
})
|
|
return tables
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail="查询表列表失败: " + str(e))
|
|
|
|
|
|
# ---- Table Detail ----
|
|
|
|
@router.get("/{instance_id}/detail/table-detail")
|
|
async def table_detail(
|
|
instance_id: int,
|
|
database: str = Query(..., description="数据库名"),
|
|
table: str = Query(..., description="表名"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin", "dba")),
|
|
):
|
|
instance = await _get_decrypted_instance(db, instance_id)
|
|
# P0: 权限检查
|
|
await check_instance_access(current_user, instance, db)
|
|
if instance.db_type != "mysql":
|
|
raise HTTPException(status_code=400, detail="仅支持MySQL实例")
|
|
try:
|
|
# SHOW CREATE TABLE
|
|
ddl_rows = await _mysql_query(
|
|
instance,
|
|
"SHOW CREATE TABLE " + table,
|
|
database=database,
|
|
)
|
|
ddl = ddl_rows[0].get("Create Table", "") if ddl_rows else ""
|
|
|
|
# SHOW COLUMNS
|
|
columns = await _mysql_query(
|
|
instance,
|
|
"SHOW COLUMNS FROM " + table,
|
|
database=database,
|
|
)
|
|
|
|
# SHOW INDEX
|
|
indexes = await _mysql_query(
|
|
instance,
|
|
"SHOW INDEX FROM " + table,
|
|
database=database,
|
|
)
|
|
|
|
return {
|
|
"ddl": ddl,
|
|
"columns": columns,
|
|
"indexes": indexes,
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail="查询表详情失败: " + str(e))
|
|
|
|
|
|
# ---- Users ----
|
|
|
|
@router.get("/{instance_id}/detail/users")
|
|
async def list_users(
|
|
instance_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin", "dba")),
|
|
):
|
|
instance = await _get_decrypted_instance(db, instance_id)
|
|
# P0: 权限检查
|
|
await check_instance_access(current_user, instance, db)
|
|
if instance.db_type != "mysql":
|
|
raise HTTPException(status_code=400, detail="仅支持MySQL实例")
|
|
try:
|
|
rows = await _mysql_query(instance, "SELECT user, host FROM mysql.user")
|
|
users = [{"user": row.get("user", ""), "host": row.get("host", "")} for row in rows]
|
|
return users
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail="查询用户列表失败: " + str(e))
|
|
|
|
|
|
# ---- Grants ----
|
|
|
|
@router.get("/{instance_id}/detail/grants")
|
|
async def show_grants(
|
|
instance_id: int,
|
|
user: str = Query(..., description="用户名"),
|
|
host: str = Query(..., description="主机"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin", "dba")),
|
|
):
|
|
instance = await _get_decrypted_instance(db, instance_id)
|
|
# P0: 权限检查
|
|
await check_instance_access(current_user, instance, db)
|
|
if instance.db_type != "mysql":
|
|
raise HTTPException(status_code=400, detail="仅支持MySQL实例")
|
|
try:
|
|
quoted_user = user.replace("'", "\\'")
|
|
quoted_host = host.replace("'", "\\'")
|
|
sql = "SHOW GRANTS FOR '" + quoted_user + "'@'" + quoted_host + "'"
|
|
rows = await _mysql_query(instance, sql)
|
|
grants = []
|
|
for row in rows:
|
|
# The key varies by MySQL version, grab first value
|
|
grants.append(list(row.values())[0])
|
|
return {"user": user, "host": host, "grants": grants}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail="查询权限失败: " + str(e))
|
|
|
|
|
|
# ---- Slow Queries ----
|
|
|
|
@router.get("/{instance_id}/detail/slow-queries")
|
|
async def slow_queries(
|
|
instance_id: int,
|
|
database: Optional[str] = Query(None, description="数据库名"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin", "dba")),
|
|
):
|
|
instance = await _get_decrypted_instance(db, instance_id)
|
|
# P0: 权限检查
|
|
await check_instance_access(current_user, instance, db)
|
|
if instance.db_type != "mysql":
|
|
raise HTTPException(status_code=400, detail="仅支持MySQL实例")
|
|
try:
|
|
# Try querying mysql.slow_log table first
|
|
try:
|
|
sql = (
|
|
"SELECT start_time, user_host, query_time, lock_time, "
|
|
"rows_sent, rows_examined, sql_text "
|
|
"FROM mysql.slow_log ORDER BY start_time DESC LIMIT 100"
|
|
)
|
|
rows = await _mysql_query(instance, sql, database=database)
|
|
return rows
|
|
except Exception:
|
|
# Fallback: show slow query variables
|
|
rows = await _mysql_query(
|
|
instance,
|
|
"SHOW VARIABLES LIKE 'slow%'",
|
|
database=database,
|
|
)
|
|
return rows
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail="查询慢查询失败: " + str(e))
|
|
|
|
|
|
# ---- Processlist ----
|
|
|
|
@router.get("/{instance_id}/detail/processlist")
|
|
async def show_processlist(
|
|
instance_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin", "dba")),
|
|
):
|
|
instance = await _get_decrypted_instance(db, instance_id)
|
|
# P0: 权限检查
|
|
await check_instance_access(current_user, instance, db)
|
|
if instance.db_type != "mysql":
|
|
raise HTTPException(status_code=400, detail="仅支持MySQL实例")
|
|
try:
|
|
rows = await _mysql_query(instance, "SHOW FULL PROCESSLIST")
|
|
return rows
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail="查询进程列表失败: " + str(e))
|
|
|
|
|
|
# ---- Kill Process ----
|
|
|
|
@router.post("/{instance_id}/detail/processlist/{pid}/kill")
|
|
async def kill_process(
|
|
instance_id: int,
|
|
pid: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin", "dba")),
|
|
):
|
|
instance = await _get_decrypted_instance(db, instance_id)
|
|
# P0: 权限检查
|
|
await check_instance_access(current_user, instance, db)
|
|
if instance.db_type != "mysql":
|
|
raise HTTPException(status_code=400, detail="仅支持MySQL实例")
|
|
try:
|
|
await _mysql_execute(instance, "KILL CONNECTION " + str(pid))
|
|
return {"message": "进程已终止", "pid": pid}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail="终止进程失败: " + str(e))
|
|
|
|
|
|
# ---- Status ----
|
|
|
|
@router.get("/{instance_id}/detail/status")
|
|
async def show_status(
|
|
instance_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin", "dba")),
|
|
):
|
|
instance = await _get_decrypted_instance(db, instance_id)
|
|
# P0: 权限检查
|
|
await check_instance_access(current_user, instance, db)
|
|
if instance.db_type != "mysql":
|
|
raise HTTPException(status_code=400, detail="仅支持MySQL实例")
|
|
try:
|
|
rows = await _mysql_query(instance, "SHOW GLOBAL STATUS")
|
|
# Return key metrics
|
|
key_metrics = {}
|
|
important_keys = {
|
|
"Threads_connected", "Threads_running", "Threads_cached",
|
|
"Queries", "Questions", "Slow_queries",
|
|
"Connections", "Aborted_connects",
|
|
"Bytes_received", "Bytes_sent",
|
|
"Table_open_cache_hits", "Table_open_cache_misses",
|
|
"Innodb_row_lock_waits", "Innodb_row_lock_time",
|
|
"Uptime",
|
|
}
|
|
for row in rows:
|
|
var_name = row.get("Variable_name", "")
|
|
if var_name in important_keys:
|
|
key_metrics[var_name] = row.get("Value", "")
|
|
return {
|
|
"key_metrics": key_metrics,
|
|
"all_status": rows,
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail="查询状态失败: " + str(e))
|
|
|
|
|
|
# ---- Variables ----
|
|
|
|
@router.get("/{instance_id}/detail/variables")
|
|
async def show_variables(
|
|
instance_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin", "dba")),
|
|
):
|
|
instance = await _get_decrypted_instance(db, instance_id)
|
|
# P0: 权限检查
|
|
await check_instance_access(current_user, instance, db)
|
|
if instance.db_type != "mysql":
|
|
raise HTTPException(status_code=400, detail="仅支持MySQL实例")
|
|
try:
|
|
rows = await _mysql_query(instance, "SHOW VARIABLES")
|
|
return rows
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail="查询变量失败: " + str(e))
|