完善部分平台治理功能,及修改看板缺陷
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from __future__ import annotations
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
@@ -442,16 +442,14 @@ async def dashboard_stats() -> dict[str, Any]:
|
||||
|
||||
running_statuses = {"syncing", "queued", "running"}
|
||||
running_ft = [t for t in tasks if t.get("status") in running_statuses]
|
||||
failed_ft = [t for t in tasks if t.get("status") == "failed"]
|
||||
all_ft = tasks # 全部训练任务(含已完成/异常)
|
||||
online_nodes = [n for n in nodes if n.get("scheduler_status") == "online"]
|
||||
# 评测中运行的任务
|
||||
running_eval = [e for e in eval_tasks if e.get("status") in running_statuses]
|
||||
# 数据处理中运行的任务
|
||||
# 评测中运行的任务数
|
||||
eval_running = 0
|
||||
try:
|
||||
dp_running = int(dp_store.list_tasks(page=1, page_size=1, status="running").get("total", 0))
|
||||
eval_tasks = store.eval_tasks()
|
||||
eval_running = len([e for e in eval_tasks if e.get("status") in running_statuses])
|
||||
except Exception:
|
||||
dp_running = 0
|
||||
eval_running = 0
|
||||
|
||||
# 近 7 天训练统计(按创建日期分桶)
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -472,45 +470,30 @@ async def dashboard_stats() -> dict[str, Any]:
|
||||
}
|
||||
)
|
||||
|
||||
# 服务状态 —— 通过对应接口连通性判断是否正常
|
||||
# 服务状态 —— 每个服务的"实例数"含义:
|
||||
# 模型训练 → 训练任务总数
|
||||
# 模型评测 → 评测任务总数
|
||||
# 模型推理 → 推理/对比任务实例数
|
||||
# 模型管理 → 基座模型注册总数
|
||||
# 数据集管理 → 数据集总数
|
||||
# 数据处理 → 数据处理任务总数
|
||||
# 数据类型转换 → 数据转换任务总数
|
||||
service_checks = [
|
||||
("模型训练", "/fine-tune", "模型训练"),
|
||||
("模型评测", "/model-eval", "模型评测"),
|
||||
("模型推理", "/model-inference", "模型推理"),
|
||||
("模型管理", "/model-manage", "模型管理"),
|
||||
("数据集管理", "/dataset-manage", "数据集管理"),
|
||||
("数据处理", "/data-process", "数据处理"),
|
||||
("数据类型转换", "/data-convert", "数据类型转换"),
|
||||
("模型训练", "fine-tune", len(tasks)),
|
||||
("模型评测", "model-eval", len(eval_tasks)),
|
||||
("模型推理", "model-inference", len(store.compare_tasks())),
|
||||
("模型管理", "model-manage", len(store.models())),
|
||||
("数据集管理", "dataset-manage", len(datasets)),
|
||||
("数据处理", "data-process", dp_count),
|
||||
("数据类型转换", "data-convert", dp_count),
|
||||
]
|
||||
service_status = []
|
||||
for svc_type, _path, _label in service_checks:
|
||||
try:
|
||||
svc_count = 0
|
||||
if svc_type == "模型训练":
|
||||
svc_count = len(tasks)
|
||||
elif svc_type == "模型评测":
|
||||
svc_count = len(eval_tasks)
|
||||
elif svc_type == "模型推理":
|
||||
svc_count = len(online_nodes)
|
||||
elif svc_type == "模型管理":
|
||||
svc_count = len(store.models())
|
||||
elif svc_type == "数据集管理":
|
||||
svc_count = len(datasets)
|
||||
elif svc_type == "数据处理":
|
||||
svc_count = dp_count
|
||||
elif svc_type == "数据类型转换":
|
||||
svc_count = dp_count
|
||||
service_status.append({
|
||||
"type": svc_type,
|
||||
"status": "normal",
|
||||
"count": svc_count,
|
||||
})
|
||||
except Exception:
|
||||
service_status.append({
|
||||
"type": svc_type,
|
||||
"status": "error",
|
||||
"count": 0,
|
||||
})
|
||||
for svc_type, _path, svc_count in service_checks:
|
||||
service_status.append({
|
||||
"type": svc_type,
|
||||
"status": "normal",
|
||||
"count": svc_count,
|
||||
})
|
||||
|
||||
# 训练任务状态归一化
|
||||
status_map = {
|
||||
@@ -590,12 +573,16 @@ async def dashboard_stats() -> dict[str, Any]:
|
||||
]
|
||||
|
||||
# 登录时长排行(本月),只取 top 5
|
||||
login_duration_rank = store.login_duration_rank(limit=5)
|
||||
login_duration_rank = []
|
||||
try:
|
||||
login_duration_rank = store.login_duration_rank(limit=5)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ok(
|
||||
{
|
||||
"online_services": sum(s["count"] for s in service_status),
|
||||
"running_tasks": len(running_ft) + len(running_eval) + dp_running,
|
||||
"running_tasks": len(running_ft) + eval_running,
|
||||
"pending_alerts": 0,
|
||||
"training_7d": training_7d,
|
||||
"service_status": service_status,
|
||||
@@ -656,6 +643,29 @@ async def reset_user_password(
|
||||
raise fail(400, str(exc))
|
||||
|
||||
|
||||
@router.post("/users/me/password")
|
||||
async def change_my_password(
|
||||
payload: dict[str, Any] = Body(...),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
"""用户自行修改密码:验证旧密码后设置新密码。"""
|
||||
old_password = payload.get("old_password") or ""
|
||||
new_password = payload.get("new_password") or ""
|
||||
if not old_password or not new_password:
|
||||
raise fail(400, "old_password and new_password are required")
|
||||
if len(new_password) < 6:
|
||||
raise fail(400, "new password must be at least 6 characters")
|
||||
try:
|
||||
success = get_platform_store().change_password(
|
||||
current_user["id"], old_password, new_password
|
||||
)
|
||||
except KeyError:
|
||||
raise fail(404, "user not found")
|
||||
if not success:
|
||||
raise fail(400, "old password is incorrect")
|
||||
return ok({"changed": True})
|
||||
|
||||
|
||||
@router.get("/model-manage/local-models")
|
||||
async def local_models() -> dict[str, Any]:
|
||||
store = get_platform_store()
|
||||
@@ -685,8 +695,15 @@ async def local_models() -> dict[str, Any]:
|
||||
|
||||
|
||||
@router.get("/model-manage/trained-models")
|
||||
async def trained_models() -> dict[str, Any]:
|
||||
return ok({"models": get_platform_store().trained_models()})
|
||||
async def trained_models(current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
||||
all_models = get_platform_store().trained_models()
|
||||
if is_admin(current_user):
|
||||
return ok({"models": all_models})
|
||||
# 普通用户只能看到自己创建的 + ACL 授权的
|
||||
user_id = current_user.get("id")
|
||||
accessible = set(filter_accessible_resource_ids("trained_model", [m["id"] for m in all_models], current_user))
|
||||
result = [m for m in all_models if m.get("created_by") == user_id or m["id"] in accessible]
|
||||
return ok({"models": result})
|
||||
|
||||
|
||||
@router.delete("/model-manage/trained-models/{model_id}")
|
||||
@@ -720,16 +737,13 @@ async def model_by_name(name: str) -> dict[str, Any]:
|
||||
|
||||
@router.get("/model-manage")
|
||||
async def model_list(current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
||||
models = get_platform_store().models()
|
||||
if current_user.get("role") == "admin" or current_user.get("protected"):
|
||||
return ok(models)
|
||||
# 普通用户只返回有 ACL 授权的模型
|
||||
accessible = set(filter_accessible_resource_ids("model", [m["id"] for m in models], current_user))
|
||||
return ok([m for m in models if m["id"] in accessible])
|
||||
# 基座模型是平台共享资源,所有登录用户均可查看
|
||||
return ok(get_platform_store().models())
|
||||
|
||||
|
||||
@router.post("/model-manage")
|
||||
async def create_model(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
async def create_model(payload: dict[str, Any] = Body(...), current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
||||
payload.setdefault("created_by", current_user.get("id"))
|
||||
try:
|
||||
return ok(get_platform_store().create_model(payload))
|
||||
except KeyError as exc:
|
||||
@@ -1047,14 +1061,18 @@ async def download_dataset_file(dataset_id: str, file_id: str, version_id: str |
|
||||
@router.get("/dataset-manage")
|
||||
async def dataset_list(current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
||||
datasets = get_platform_store().datasets()
|
||||
if current_user.get("role") == "admin" or current_user.get("protected"):
|
||||
if is_admin(current_user):
|
||||
return ok(datasets)
|
||||
# 普通用户可见:自己创建的 + ACL 授权的
|
||||
user_id = current_user.get("id")
|
||||
accessible = set(filter_accessible_resource_ids("dataset", [d["id"] for d in datasets], current_user))
|
||||
return ok([d for d in datasets if d["id"] in accessible])
|
||||
result = [d for d in datasets if d.get("created_by") == user_id or d["id"] in accessible]
|
||||
return ok(result)
|
||||
|
||||
|
||||
@router.post("/dataset-manage")
|
||||
async def create_dataset(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
async def create_dataset(payload: dict[str, Any] = Body(...), current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
||||
payload.setdefault("created_by", current_user.get("id"))
|
||||
dataset = get_platform_store().create_dataset(payload)
|
||||
return ok({"id": dataset["id"]})
|
||||
|
||||
@@ -1127,8 +1145,20 @@ async def create_fine_tune(payload: dict[str, Any] = Body(...)) -> dict[str, Any
|
||||
|
||||
|
||||
@router.post("/fine-tune/start")
|
||||
async def start_fine_tune(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
async def start_fine_tune(
|
||||
payload: dict[str, Any] = Body(...),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
store = get_platform_store()
|
||||
# GPU 权限校验:普通用户只能使用被分配的 GPU
|
||||
if not is_admin(current_user):
|
||||
node_id = payload.get("compute_node_id") or payload.get("node_id")
|
||||
gpu_indices = payload.get("gpus") or []
|
||||
if node_id and gpu_indices:
|
||||
if not store.check_gpu_access(current_user["id"], node_id, gpu_indices):
|
||||
raise fail(403, "无权使用所选 GPU,请联系管理员分配")
|
||||
# 记录创建者
|
||||
payload.setdefault("created_by", current_user.get("id"))
|
||||
try:
|
||||
return ok(await _submit_fine_tune_task(store, payload))
|
||||
except KeyError:
|
||||
@@ -1267,10 +1297,14 @@ async def update_fine_tune(task_id: str, payload: dict[str, Any] = Body(...)) ->
|
||||
|
||||
|
||||
@router.post("/fine-tune/stop/{task_id}")
|
||||
async def stop_fine_tune(task_id: str) -> dict[str, Any]:
|
||||
async def stop_fine_tune(task_id: str, current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
||||
store = get_platform_store()
|
||||
try:
|
||||
task = store.task(task_id)
|
||||
# 审批拦截:非 admin 停止他人任务需审批
|
||||
pending = _require_approval_or_admin("fine_tune_task", task_id, current_user, f"停止训练任务 {task_id}")
|
||||
if pending:
|
||||
return pending
|
||||
node = _node_for_task(task)
|
||||
if task.get("compute_job_id") and node and get_settings().compute_mode != "simulator":
|
||||
job = await ComputeNodeClient(node["api_base_url"]).stop_job(task["compute_job_id"])
|
||||
@@ -1281,8 +1315,8 @@ async def stop_fine_tune(task_id: str) -> dict[str, Any]:
|
||||
|
||||
|
||||
@router.post("/fine-tune/{task_id}/stop")
|
||||
async def stop_fine_tune_alt(task_id: str) -> dict[str, Any]:
|
||||
return await stop_fine_tune(task_id)
|
||||
async def stop_fine_tune_alt(task_id: str, current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
||||
return await stop_fine_tune(task_id, current_user)
|
||||
|
||||
|
||||
@router.post("/fine-tune/{task_id}/retry")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.endpoints.data_process import router as data_process_router
|
||||
from app.api.v1.endpoints.platform import router as platform_router
|
||||
@@ -9,6 +9,7 @@ from app.modules.approval.router import router as approval_router
|
||||
from app.modules.system.router import router as system_router
|
||||
from app.modules.retention.router import router as retention_router
|
||||
from app.modules.resource.router import router as resource_router
|
||||
from app.modules.gpu.router import router as gpu_router
|
||||
from app.modules.data_convert.router import router as data_convert_router
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -21,4 +22,5 @@ api_router.include_router(project_router, tags=["project"])
|
||||
api_router.include_router(approval_router, tags=["approval"])
|
||||
api_router.include_router(retention_router, tags=["retention"])
|
||||
api_router.include_router(resource_router, tags=["resource"])
|
||||
api_router.include_router(gpu_router, tags=["gpu-assignment"])
|
||||
api_router.include_router(data_convert_router, tags=["data-convert"])
|
||||
|
||||
@@ -525,7 +525,7 @@ class PlatformStore:
|
||||
},
|
||||
)
|
||||
schema_dir = Path(__file__).with_name("sql")
|
||||
for extra in ("002_governance.sql", "003_tenant_quota.sql"):
|
||||
for extra in ("002_governance.sql", "003_tenant_quota.sql", "004_permissions.sql"):
|
||||
extra_path = schema_dir / extra
|
||||
if extra_path.exists():
|
||||
conn.executescript(extra_path.read_text(encoding="utf-8"))
|
||||
@@ -667,8 +667,8 @@ class PlatformStore:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO trained_models
|
||||
(id, name, train_methods, base_model_path, create_time, merged, merging, merged_path, artifact_dir, compute_node_id, compute_node_name)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(id, name, train_methods, base_model_path, create_time, merged, merging, merged_path, artifact_dir, compute_node_id, compute_node_name, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
trained_model_id,
|
||||
@@ -682,6 +682,7 @@ class PlatformStore:
|
||||
output_dir,
|
||||
task.get("compute_node_id"),
|
||||
task.get("compute_node_code") or task.get("compute_node_name"),
|
||||
task.get("created_by"),
|
||||
),
|
||||
)
|
||||
# Use real artifact data from compute node when available
|
||||
@@ -1284,10 +1285,22 @@ class PlatformStore:
|
||||
row = conn.execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone()
|
||||
if not row:
|
||||
raise KeyError(user_id)
|
||||
is_admin = row["role"] == "admin" or bool(row["protected"])
|
||||
if "permissions" in payload:
|
||||
perms = payload["permissions"]
|
||||
if is_admin:
|
||||
# 管理员权限不可更改,必须是全部
|
||||
perms = ALL_PERMISSIONS
|
||||
else:
|
||||
# 非 admin 用户不能拥有 user-settings 权限
|
||||
perms = [p for p in (perms or []) if p != "user-settings"]
|
||||
payload = {**payload, "permissions": perms}
|
||||
values = {
|
||||
"role": payload.get("role", row["role"]),
|
||||
"status": payload.get("status", row["status"]),
|
||||
"permissions": json_dumps(payload.get("permissions", json_loads(row["permissions"], []))),
|
||||
"permissions": json_dumps(
|
||||
payload.get("permissions", json_loads(row["permissions"], []))
|
||||
),
|
||||
}
|
||||
conn.execute(
|
||||
"UPDATE users SET role=?, status=?, permissions=? WHERE id=?",
|
||||
@@ -1302,6 +1315,34 @@ class PlatformStore:
|
||||
raise KeyError(user_id)
|
||||
if row["protected"]:
|
||||
raise ValueError("protected user cannot be deleted")
|
||||
# 级联删除该用户关联的数据
|
||||
tables_to_clean = [
|
||||
# ACL 授权
|
||||
("acls", "principal_type='user' AND principal_id=?", [user_id]),
|
||||
# 审批实例(申请人)
|
||||
("approval_instances", "applicant_id=?", [user_id]),
|
||||
# 审计日志
|
||||
("audit_logs", "actor_id=?", [user_id]),
|
||||
# 项目成员
|
||||
("project_members", "user_id=?", [user_id]),
|
||||
# GPU 分配
|
||||
("gpu_assignments", "user_id=?", [user_id]),
|
||||
# 数据集
|
||||
("datasets", "created_by=?", [user_id]),
|
||||
# 基座模型
|
||||
("models", "created_by=?", [user_id]),
|
||||
# 微调产物
|
||||
("trained_models", "created_by=?", [user_id]),
|
||||
# 评测任务
|
||||
("eval_tasks", "created_by=?", [user_id]),
|
||||
# 对比/推理任务(payload 中 creator)
|
||||
# 训练任务:仅标记为已删除或保留(有 compute_job_id 关联),不清物理数据
|
||||
]
|
||||
for table_name, where_clause, params in tables_to_clean:
|
||||
try:
|
||||
conn.execute(f"DELETE FROM {table_name} WHERE {where_clause}", params)
|
||||
except Exception:
|
||||
pass # 表可能不存在或字段不存在,跳过
|
||||
conn.execute("DELETE FROM users WHERE id=?", (user_id,))
|
||||
|
||||
def reset_password(self, user_id: str, new_password: str) -> None:
|
||||
@@ -1357,8 +1398,8 @@ class PlatformStore:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO models
|
||||
(id, name, type, purpose, model_source, description, path, api_url, api_key, online_model_name, can_train, create_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(id, name, type, purpose, model_source, description, path, api_url, api_key, online_model_name, can_train, create_time, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
model_id,
|
||||
@@ -1373,6 +1414,7 @@ class PlatformStore:
|
||||
payload.get("online_model_name"),
|
||||
can_train,
|
||||
utcnow(),
|
||||
payload.get("created_by"),
|
||||
),
|
||||
)
|
||||
return dict(conn.execute("SELECT * FROM models WHERE id=?", (model_id,)).fetchone())
|
||||
@@ -1540,8 +1582,8 @@ class PlatformStore:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO datasets
|
||||
(id, name, type, storage_type, source, task_id, size, count, description, create_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(id, name, type, storage_type, source, task_id, size, count, description, create_time, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
dataset_id,
|
||||
@@ -1554,6 +1596,7 @@ class PlatformStore:
|
||||
payload.get("count", 0),
|
||||
payload.get("description"),
|
||||
utcnow(),
|
||||
payload.get("created_by"),
|
||||
),
|
||||
)
|
||||
return self._dataset(conn, conn.execute("SELECT * FROM datasets WHERE id=?", (dataset_id,)).fetchone())
|
||||
@@ -4074,6 +4117,130 @@ class PlatformStore:
|
||||
"DELETE FROM retention_policies WHERE id=?", (policy_id,)
|
||||
)
|
||||
|
||||
# ===================== 平台治理:GPU 算力分配 =====================
|
||||
|
||||
def gpu_assignments(self) -> list[dict[str, Any]]:
|
||||
"""查询全部分配关系。"""
|
||||
with self.connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT ga.*, u.username, u.display_name,
|
||||
n.code AS node_code, n.name AS node_name, g.name AS gpu_name
|
||||
FROM gpu_assignments ga
|
||||
LEFT JOIN users u ON u.id = ga.user_id
|
||||
LEFT JOIN compute_nodes n ON n.id = ga.node_id
|
||||
LEFT JOIN gpus g ON g.node_id = ga.node_id AND g.gpu_index = ga.gpu_index
|
||||
ORDER BY ga.assigned_at DESC
|
||||
"""
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def gpu_assignments_for_user(self, user_id: str) -> list[dict[str, Any]]:
|
||||
"""查询某用户被分配的 GPU 列表。"""
|
||||
with self.connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT ga.node_id, ga.gpu_index,
|
||||
n.code AS node_code, n.name AS node_name,
|
||||
g.name AS gpu_name, g.uuid, g.memory_total_gb
|
||||
FROM gpu_assignments ga
|
||||
JOIN compute_nodes n ON n.id = ga.node_id
|
||||
LEFT JOIN gpus g ON g.node_id = ga.node_id AND g.gpu_index = ga.gpu_index
|
||||
WHERE ga.user_id = ?
|
||||
ORDER BY n.code, ga.gpu_index
|
||||
""",
|
||||
(user_id,),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def assign_gpus(self, assignments: list[dict[str, Any]], assigned_by: str | None = None) -> list[dict[str, Any]]:
|
||||
"""批量分配 GPU(幂等:已存在的分配跳过)。"""
|
||||
now = utcnow()
|
||||
with self.connect() as conn:
|
||||
for a in assignments:
|
||||
node_id = a["node_id"]
|
||||
gpu_index = a["gpu_index"]
|
||||
user_id = a["user_id"]
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM gpu_assignments WHERE node_id=? AND gpu_index=? AND user_id=?",
|
||||
(node_id, gpu_index, user_id),
|
||||
).fetchone()
|
||||
if existing:
|
||||
continue
|
||||
aid = new_id("ga")
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO gpu_assignments (id, node_id, gpu_index, user_id, assigned_by, assigned_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(aid, node_id, gpu_index, user_id, assigned_by, now),
|
||||
)
|
||||
return self.gpu_assignments()
|
||||
|
||||
def unassign_gpu(self, assignment_id: str) -> None:
|
||||
with self.connect() as conn:
|
||||
conn.execute("DELETE FROM gpu_assignments WHERE id=?", (assignment_id,))
|
||||
|
||||
def check_gpu_access(self, user_id: str, node_id: str, gpu_indices: list[int]) -> bool:
|
||||
"""检查用户是否被分配了指定节点的指定 GPU 卡。"""
|
||||
if not gpu_indices:
|
||||
return True
|
||||
with self.connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT gpu_index FROM gpu_assignments
|
||||
WHERE user_id=? AND node_id=?
|
||||
""",
|
||||
(user_id, node_id),
|
||||
).fetchall()
|
||||
assigned = {r["gpu_index"] for r in rows}
|
||||
return all(idx in assigned for idx in gpu_indices)
|
||||
|
||||
# ===================== 平台治理:资源可见性过滤 =====================
|
||||
|
||||
def _filter_accessible_ids(
|
||||
self, resource_type: str, all_ids: list[str], user: dict[str, Any]
|
||||
) -> list[str]:
|
||||
"""从全部资源 ID 中过滤出当前用户可访问的 ID 列表。
|
||||
- admin 直接返回全部。
|
||||
- 资源所有者可见(需调用方在 all_ids 中提供 owned ids)。
|
||||
- ACL 授权的用户/角色可见。
|
||||
"""
|
||||
if user.get("role") == "admin" or user.get("protected"):
|
||||
return all_ids
|
||||
if not all_ids:
|
||||
return []
|
||||
user_id = user.get("id")
|
||||
user_role = user.get("role")
|
||||
with self.connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT DISTINCT resource_id FROM acls
|
||||
WHERE resource_type=? AND (
|
||||
(principal_type='user' AND principal_id=?)
|
||||
OR (principal_type='role' AND principal_id=?)
|
||||
)
|
||||
""",
|
||||
(resource_type, user_id, user_role),
|
||||
).fetchall()
|
||||
accessible = {r["resource_id"] for r in rows}
|
||||
return [rid for rid in all_ids if rid in accessible]
|
||||
|
||||
def change_password(self, user_id: str, old_password: str, new_password: str) -> bool:
|
||||
"""用户自行修改密码:验证旧密码后设置新密码。"""
|
||||
with self.connect() as conn:
|
||||
row = conn.execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone()
|
||||
if not row:
|
||||
raise KeyError(user_id)
|
||||
matched, _ = verify_password(old_password, row["password_hash"])
|
||||
if not matched:
|
||||
return False
|
||||
conn.execute(
|
||||
"UPDATE users SET password_hash=? WHERE id=?",
|
||||
(hash_password(new_password), user_id),
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
_store: PlatformStore | None = None
|
||||
|
||||
|
||||
22
backend/app/db/sql/004_permissions.sql
Normal file
22
backend/app/db/sql/004_permissions.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- ============================================================
|
||||
-- 权限体系扩展:GPU 分配表 + 资源所有权字段
|
||||
-- ============================================================
|
||||
|
||||
-- GPU 分配表:管理员指定哪些用户可以使用哪些 GPU 卡
|
||||
CREATE TABLE IF NOT EXISTS gpu_assignments (
|
||||
id TEXT PRIMARY KEY,
|
||||
node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE,
|
||||
gpu_index INTEGER NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
assigned_by TEXT,
|
||||
assigned_at TEXT NOT NULL,
|
||||
UNIQUE (node_id, gpu_index, user_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_gpu_assignments_user ON gpu_assignments(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_gpu_assignments_gpu ON gpu_assignments(node_id, gpu_index);
|
||||
|
||||
-- 资源所有权字段:用户创建的数据集/模型/训练产物/评测任务
|
||||
ALTER TABLE datasets ADD COLUMN IF NOT EXISTS created_by TEXT;
|
||||
ALTER TABLE models ADD COLUMN IF NOT EXISTS created_by TEXT;
|
||||
ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS created_by TEXT;
|
||||
ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS created_by TEXT;
|
||||
1
backend/app/modules/gpu/__init__.py
Normal file
1
backend/app/modules/gpu/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""GPU assignment management module."""
|
||||
77
backend/app/modules/gpu/router.py
Normal file
77
backend/app/modules/gpu/router.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""GPU 算力分配管理路由。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Request
|
||||
|
||||
from app.api.v1.endpoints.platform import ok, fail
|
||||
from app.core.auth import get_current_user, is_admin
|
||||
from app.db.platform_store import get_platform_store
|
||||
|
||||
router = APIRouter(prefix="/compute", tags=["gpu-assignment"])
|
||||
|
||||
|
||||
def _actor_id(request: Request) -> str | None:
|
||||
auth = request.headers.get("Authorization", "")
|
||||
token = auth.replace("Bearer ", "").strip()
|
||||
if token.startswith("platform-token-"):
|
||||
return token[len("platform-token-"):]
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/gpu-assignments")
|
||||
def list_assignments(current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
||||
"""查看全部分配关系(仅 admin)。"""
|
||||
if not is_admin(current_user):
|
||||
raise fail(403, "admin permission required")
|
||||
return ok(get_platform_store().gpu_assignments())
|
||||
|
||||
|
||||
@router.post("/gpu-assignments")
|
||||
def assign_gpus(
|
||||
payload: dict[str, Any] = Body(...),
|
||||
request: Request = None,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
"""批量分配 GPU(仅 admin)。body: { assignments: [{ node_id, gpu_index, user_id }] }"""
|
||||
if not is_admin(current_user):
|
||||
raise fail(403, "admin permission required")
|
||||
assignments = payload.get("assignments") or []
|
||||
if not assignments:
|
||||
raise fail(400, "assignments 不能为空")
|
||||
actor = _actor_id(request) if request else None
|
||||
result = get_platform_store().assign_gpus(assignments, assigned_by=actor)
|
||||
get_platform_store().record_audit(
|
||||
action="gpu.assign",
|
||||
actor_id=actor,
|
||||
target_type="gpu",
|
||||
detail=f"count={len(assignments)}",
|
||||
)
|
||||
return ok(result)
|
||||
|
||||
|
||||
@router.delete("/gpu-assignments/{assignment_id}")
|
||||
def unassign_gpu(
|
||||
assignment_id: str,
|
||||
request: Request = None,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
"""撤销 GPU 分配(仅 admin)。"""
|
||||
if not is_admin(current_user):
|
||||
raise fail(403, "admin permission required")
|
||||
get_platform_store().unassign_gpu(assignment_id)
|
||||
actor = _actor_id(request) if request else None
|
||||
get_platform_store().record_audit(
|
||||
action="gpu.unassign",
|
||||
actor_id=actor,
|
||||
target_type="gpu",
|
||||
target_id=assignment_id,
|
||||
)
|
||||
return ok({"deleted": assignment_id})
|
||||
|
||||
|
||||
@router.get("/my-gpus")
|
||||
def my_gpus(current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
||||
"""查看当前用户可用的 GPU 列表。"""
|
||||
return ok(get_platform_store().gpu_assignments_for_user(current_user["id"]))
|
||||
@@ -138,6 +138,14 @@ class FakePlatformStore:
|
||||
return dict(u)
|
||||
return None
|
||||
|
||||
def create_session(self, user_id: str) -> dict[str, Any]:
|
||||
import secrets
|
||||
sid = secrets.token_hex(16)
|
||||
return {"session_id": sid, "user_id": user_id}
|
||||
|
||||
def finish_session(self, session_id: str) -> None:
|
||||
pass
|
||||
|
||||
def users(self) -> list[dict[str, Any]]:
|
||||
return [dict(u) for u in self._users]
|
||||
|
||||
@@ -392,12 +400,21 @@ class FakePlatformStore:
|
||||
def tasks(self) -> list[dict[str, Any]]:
|
||||
return self._tasks
|
||||
|
||||
def eval_tasks(self) -> list[dict[str, Any]]:
|
||||
return []
|
||||
|
||||
def compute_nodes(self) -> list[dict[str, Any]]:
|
||||
return self._compute_nodes
|
||||
|
||||
def gpus(self) -> list[dict[str, Any]]:
|
||||
return self._gpus
|
||||
|
||||
def compare_tasks(self) -> list[dict[str, Any]]:
|
||||
return []
|
||||
|
||||
def trained_models(self) -> list[dict[str, Any]]:
|
||||
return []
|
||||
|
||||
def system_info(self) -> dict[str, Any]:
|
||||
return {"cpu": {}, "memory": {}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user