完善部分平台治理功能,及修改看板缺陷
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user