init: pcc module skeleton — models, CRUD, API, load_path

This commit is contained in:
yumoqing 2026-08-07 11:23:19 +08:00
commit 64a3dfd9e8
23 changed files with 692 additions and 0 deletions

47
json/cluster_list.json Normal file
View File

@ -0,0 +1,47 @@
{
"tblname": "cluster",
"alias": "cluster_list",
"title": "集群管理",
"params": {
"browserfields": {
"name": {
"title": "集群名称",
"width": 150
},
"cluster_type": {
"title": "集群类型",
"width": 100
},
"pool_id": {
"title": "算力池",
"width": 150
},
"version": {
"title": "版本",
"width": 100
},
"endpoint": {
"title": "入口",
"width": 200
},
"status": {
"title": "状态",
"width": 100
}
},
"editexclouded": [
"id",
"resellerid",
"created_at",
"updated_at"
],
"toolbar": {
"tools": []
},
"binds": [],
"new_data_url": "{{entire_url('/pcc/api/cluster_create.dspy')}}",
"update_data_url": "{{entire_url('/pcc/api/cluster_update.dspy')}}",
"delete_data_url": "{{entire_url('/pcc/api/cluster_delete.dspy')}}",
"logined_userorgid": "resellerid"
}
}

View File

@ -0,0 +1,39 @@
{
"tblname": "cluster_node",
"alias": "cluster_node_list",
"title": "节点分配",
"params": {
"browserfields": {
"cluster_id": {
"title": "集群",
"width": 150
},
"node_id": {
"title": "节点",
"width": 150
},
"role": {
"title": "角色",
"width": 100
},
"status": {
"title": "状态",
"width": 100
}
},
"editexclouded": [
"id",
"resellerid",
"created_at",
"updated_at"
],
"toolbar": {
"tools": []
},
"binds": [],
"new_data_url": "{{entire_url('/cluster_node/api/cluster_node_create.dspy')}}",
"update_data_url": "{{entire_url('/cluster_node/api/cluster_node_update.dspy')}}",
"delete_data_url": "{{entire_url('/cluster_node/api/cluster_node_delete.dspy')}}",
"logined_userorgid": "resellerid"
}
}

116
models/cluster.json Normal file
View File

@ -0,0 +1,116 @@
{
"summary": [
{
"name": "cluster",
"title": "集群",
"primary": [
"id"
],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "resellerid",
"title": "商户机构id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "pool_id",
"title": "所属算力池",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "集群名称",
"type": "str",
"length": 128,
"nullable": "no"
},
{
"name": "cluster_type",
"title": "集群类型(k8s/slurm/ray)",
"type": "char",
"length": 16,
"nullable": "no"
},
{
"name": "version",
"title": "版本号",
"type": "str",
"length": 64
},
{
"name": "endpoint",
"title": "API入口地址",
"type": "str",
"length": 256
},
{
"name": "control_config",
"title": "控制节点配置(JSON)",
"type": "text"
},
{
"name": "compute_config",
"title": "算力节点配置(JSON)",
"type": "text"
},
{
"name": "status",
"title": "状态(deploying/running/failed/stopped/destroyed)",
"type": "char",
"length": 16,
"default": "deploying"
},
{
"name": "deploy_log",
"title": "部署日志(JSON)",
"type": "text"
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp",
"nullable": "no"
}
],
"codes": [
{
"field": "pool_id",
"table": "compute_pool",
"valuefield": "id",
"textfield": "name"
},
{
"field": "cluster_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='cluster_type'"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='cluster_status'"
}
]
}

83
models/cluster_node.json Normal file
View File

@ -0,0 +1,83 @@
{
"summary": [
{
"name": "cluster_node",
"title": "集群节点分配",
"primary": [
"id"
],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "cluster_id",
"title": "所属集群",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "node_id",
"title": "算力节点",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "role",
"title": "角色(control/compute/storage)",
"type": "char",
"length": 16,
"nullable": "no"
},
{
"name": "status",
"title": "状态(joining/active/draining/removed)",
"type": "char",
"length": 16,
"default": "joining"
},
{
"name": "assigned_at",
"title": "分配时间",
"type": "timestamp",
"nullable": "no"
}
],
"codes": [
{
"field": "cluster_id",
"table": "cluster",
"valuefield": "id",
"textfield": "name"
},
{
"field": "node_id",
"table": "compute_node",
"valuefield": "id",
"textfield": "name"
},
{
"field": "role",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='node_role'"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='node_status'"
}
]
}

211
pcc/__init__.py Normal file
View File

@ -0,0 +1,211 @@
"""
pcc 统一集群管理 (Pooled Computing Cluster)
生命周期: deploy run stop start destroy
插件: k8s_plugin, slurm_plugin, ray_plugin
"""
import datetime, json
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
MODULE_NAME = 'pccs'
_PLUGINS = {
'k8s': 'pcc.k8s_plugin',
'slurm': 'pcc.slurm_plugin',
'ray': 'pcc.ray_plugin',
}
def _get_plugin(cluster_type):
import importlib
modname = _PLUGINS.get(cluster_type)
if not modname:
raise ValueError(f'Unknown cluster_type: {cluster_type}')
return importlib.import_module(modname)
async def deploy_cluster(request, cluster_id):
"""部署集群: 读取集群配置 → 分配节点 → 调用 plugin 部署"""
env = request._run_ns
dbname = env.get_module_dbname(MODULE_NAME)
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('cluster', {'id': cluster_id})
if not recs:
return {'status': 'error', 'message': f'Cluster {cluster_id} not found'}
cluster = recs[0]
# 更新状态为 deploying
await sor.U('cluster', {'id': cluster_id},
{'status': 'deploying', 'updated_at': datetime.datetime.now().isoformat()})
# 获取已分配的节点
node_recs = await sor.R('cluster_node', {'cluster_id': cluster_id})
control_nodes = [await sor.R('compute_node', {'id': n.node_id}) for n in node_recs if n.role == 'control']
compute_nodes = [await sor.R('compute_node', {'id': n.node_id}) for n in node_recs if n.role == 'compute']
control_nodes = [n[0] for n in control_nodes if n]
compute_nodes = [n[0] for n in compute_nodes if n]
if not control_nodes:
return {'status': 'error', 'message': 'No control node assigned'}
config = {}
if cluster.control_config:
try:
config = json.loads(cluster.control_config)
except:
pass
# 调用对应 plugin 部署
try:
plugin = _get_plugin(cluster.cluster_type)
deploy_log = await plugin.deploy_cluster(env, cluster_id, control_nodes, compute_nodes, config)
except Exception as e:
async with DBPools().sqlorContext(dbname) as sor:
await sor.U('cluster', {'id': cluster_id},
{'status': 'failed', 'deploy_log': str(e),
'updated_at': datetime.datetime.now().isoformat()})
return {'status': 'error', 'message': f'Deploy failed: {e}'}
# 更新状态为 running
endpoint = deploy_log.get('endpoint', '') if isinstance(deploy_log, dict) else ''
async with DBPools().sqlorContext(dbname) as sor:
await sor.U('cluster', {'id': cluster_id},
{'status': 'running', 'endpoint': endpoint,
'deploy_log': json.dumps(deploy_log) if isinstance(deploy_log, dict) else str(deploy_log),
'updated_at': datetime.datetime.now().isoformat()})
# 更新节点状态为 active
for n in control_nodes + compute_nodes:
await sor.U('cluster_node', {'cluster_id': cluster_id, 'node_id': n.id},
{'status': 'active'})
return {'status': 'ok', 'message': 'Cluster deployed', 'endpoint': endpoint}
async def add_cluster_node(request, params_kw):
"""向运行中集群动态添加节点"""
env = request._run_ns
dbname = env.get_module_dbname(MODULE_NAME)
cluster_id = params_kw.get('cluster_id', '')
node_id = params_kw.get('node_id', '')
role = params_kw.get('role', 'compute')
async with DBPools().sqlorContext(dbname) as sor:
cluster = await sor.R('cluster', {'id': cluster_id})
if not cluster:
return {'status': 'error', 'message': 'Cluster not found'}
cluster = cluster[0]
node = await sor.R('compute_node', {'id': node_id})
if not node:
return {'status': 'error', 'message': 'Node not found'}
node = node[0]
# 更新节点状态
await sor.U('compute_node', {'id': node_id},
{'status': 'allocated', 'updated_at': datetime.datetime.now().isoformat()})
# 记录分配
await sor.C('cluster_node', {
'id': getID(), 'cluster_id': cluster_id, 'node_id': node_id,
'role': role, 'status': 'joining',
'assigned_at': datetime.datetime.now().isoformat()
})
# 调用 plugin 添加
plugin = _get_plugin(cluster.cluster_type)
result = await plugin.add_node(env, cluster_id, node_id, role)
return result
async def remove_cluster_node(request, params_kw):
"""从集群移除节点并回收"""
env = request._run_ns
dbname = env.get_module_dbname(MODULE_NAME)
cluster_id = params_kw.get('cluster_id', '')
node_id = params_kw.get('node_id', '')
async with DBPools().sqlorContext(dbname) as sor:
cluster = await sor.R('cluster', {'id': cluster_id})
if not cluster:
return {'status': 'error', 'message': 'Cluster not found'}
plugin = _get_plugin(cluster[0].cluster_type)
result = await plugin.remove_node(env, cluster_id, node_id)
# 回收节点
async with DBPools().sqlorContext(dbname) as sor:
await sor.U('compute_node', {'id': node_id},
{'status': 'available', 'updated_at': datetime.datetime.now().isoformat()})
await sor.U('cluster_node', {'cluster_id': cluster_id, 'node_id': node_id},
{'status': 'removed'})
return result
async def cluster_status(request, cluster_id):
"""获取集群运行状态"""
env = request._run_ns
dbname = env.get_module_dbname(MODULE_NAME)
async with DBPools().sqlorContext(dbname) as sor:
cluster = await sor.R('cluster', {'id': cluster_id})
if not cluster:
return {'status': 'error', 'message': 'Cluster not found'}
cluster = cluster[0]
nodes = await sor.R('cluster_node', {'cluster_id': cluster_id})
# 从 plugin 获取实时状态
try:
plugin = _get_plugin(cluster.cluster_type)
live_status = await plugin.cluster_status(env, cluster_id)
except:
live_status = {}
return {
'status': 'ok',
'data': {
'id': cluster.id, 'name': cluster.name, 'type': cluster.cluster_type,
'version': cluster.version, 'endpoint': cluster.endpoint,
'state': cluster.status,
'nodes': [{'id': n.node_id, 'role': n.role, 'status': n.status} for n in nodes],
'live': live_status
}
}
async def stop_cluster(request, cluster_id):
"""停止集群"""
env = request._run_ns
dbname = env.get_module_dbname(MODULE_NAME)
async with DBPools().sqlorContext(dbname) as sor:
await sor.U('cluster', {'id': cluster_id},
{'status': 'stopped', 'updated_at': datetime.datetime.now().isoformat()})
return {'status': 'ok', 'message': 'Cluster stopped'}
async def start_cluster(request, cluster_id):
"""启动已停止的集群"""
env = request._run_ns
dbname = env.get_module_dbname(MODULE_NAME)
async with DBPools().sqlorContext(dbname) as sor:
await sor.U('cluster', {'id': cluster_id},
{'status': 'running', 'updated_at': datetime.datetime.now().isoformat()})
return {'status': 'ok', 'message': 'Cluster started'}
async def destroy_cluster(request, cluster_id):
"""销毁集群,回收所有节点"""
env = request._run_ns
dbname = env.get_module_dbname(MODULE_NAME)
async with DBPools().sqlorContext(dbname) as sor:
# 释放所有节点
nodes = await sor.R('cluster_node', {'cluster_id': cluster_id})
for n in nodes:
await sor.U('compute_node', {'id': n.node_id},
{'status': 'available', 'updated_at': datetime.datetime.now().isoformat()})
await sor.U('cluster_node', {'id': n.id}, {'status': 'removed'})
await sor.U('cluster', {'id': cluster_id},
{'status': 'destroyed', 'updated_at': datetime.datetime.now().isoformat()})
return {'status': 'ok', 'message': 'Cluster destroyed'}

View File

@ -0,0 +1,24 @@
'''
k8s_plugin - K8s + VM 集群管理
- K8s 部署 (kubeadm/k3s + 控制平面 + worker)
- VM 生命周期 (创建/快照/迁移/销毁)
- 节点加入/退出集群
- kubectl / k8s API 状态监控
- 算力单元: namespace + ResourceQuota 分配回收
''')
async def deploy_k8s_cluster(env, cluster_id, control_nodes, compute_nodes, config):
'''部署 K8s 集群: 初始化控制平面 → join worker → 验证'''
pass
async def add_node(env, cluster_id, node_id, role='compute'):
'''向已有集群添加节点'''
pass
async def remove_node(env, cluster_id, node_id):
'''从集群移除节点并回收资源'''
pass
async def cluster_status(env, cluster_id):
'''获取集群状态 (节点数、Pod数、资源使用率)'''
pass

View File

@ -0,0 +1,23 @@
'''
ray_plugin - Ray 集群管理
- Ray 集群部署 (ray start --head / --worker)
- 自动扩缩容 (Ray Autoscaler)
- Dashboard + Prometheus 监控
- 算力单元: Ray resource 标签分配回收
'''
async def deploy_ray_cluster(env, cluster_id, control_nodes, compute_nodes, config):
'''部署 Ray 集群: head node → worker nodes'''
pass
async def add_node(env, cluster_id, node_id, role='compute'):
'''向 Ray 集群添加 worker'''
pass
async def remove_node(env, cluster_id, node_id):
'''从 Ray 集群移除 worker'''
pass
async def cluster_status(env, cluster_id):
'''Ray Dashboard API 状态'''
pass

View File

@ -0,0 +1,24 @@
'''
slurm_plugin - Slurm 集群管理
- Slurm 部署 (slurmctld + slurmd + munge + 共享存储)
- Partition 管理 / QoS / TRES
- 节点 drain/resume (算力回收/分配)
- sinfo / squeue / scontrol 监控
'''
'')
async def deploy_slurm_cluster(env, cluster_id, control_nodes, compute_nodes, config):
'''部署 Slurm 集群: 控制节点 slurmctld + munge, 算力节点 slurmd'''
pass
async def add_node(env, cluster_id, node_id, role='compute'):
'''向分区添加节点'''
pass
async def remove_node(env, cluster_id, node_id):
'''drain 节点后从集群移除'''
pass
async def cluster_status(env, cluster_id):
'''sinfo + squeue 汇总'''
pass

68
scripts/load_path.py Normal file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""pcc RBAC 权限管理"""
import subprocess, os, sys, json, glob
def find_sage_root():
for c in [os.path.expanduser("~/sage"), os.path.expanduser("~/repos/sage")]:
if os.path.isdir(os.path.join(c, "py3")): return c
return None
SAGE = find_sage_root()
if not SAGE: sys.exit("Sage root not found")
PY = os.path.join(SAGE, "py3", "bin", "python")
SET = os.path.join(SAGE, "set_role_perm.py")
JSON_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "json")
API_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "wwwroot", "api")
def load_cruds():
defs = []
for f in sorted(glob.glob(os.path.join(JSON_DIR, "*.json"))):
try:
d = json.load(open(f))
defs.append({"tblname": d["tblname"], "alias": d.get("alias", d["tblname"]), "subtables": d.get("params", {}).get("subtables", [])})
except: pass
return defs
def get_apis():
if not os.path.isdir(API_DIR): return []
return [f"/pcc/api/{f}" for f in sorted(os.listdir(API_DIR)) if f.endswith(".dspy")]
cruds = load_cruds()
apis = get_apis()
PATHS_ANY = [
f"/pcc/menu.ui",
]
PATHS_LOGINED = [
f"/pcc",
f"/pcc/index.ui",
]
for d in cruds:
PATHS_ANY.append(f"/pcc/{d['alias']}")
PATHS_LOGINED.append(f"/pcc/{d['alias']}/index.ui")
for act in ["get", "add", "update", "delete"]:
PATHS_LOGINED.append(f"/pcc/{d['alias']}/{act}_{d['tblname']}.dspy")
for api in apis:
PATHS_LOGINED.append(api)
PATHS_OPERATOR = list(PATHS_LOGINED)
PATHS_ANY = list(dict.fromkeys(PATHS_ANY))
PATHS_LOGINED = list(dict.fromkeys(PATHS_LOGINED))
PATHS_OPERATOR = list(dict.fromkeys(PATHS_OPERATOR))
def reg(role, paths):
ok = 0
for p in paths:
r = subprocess.run([PY, SET, role, p], capture_output=True, text=True)
if r.returncode == 0: ok += 1
print(f" {role}: {ok}/{len(paths)}")
return ok
total = 0
print(f"{mod_name}: any={len(PATHS_ANY)} logined={len(PATHS_LOGINED)} operator={len(PATHS_OPERATOR)}")
total += reg("any", PATHS_ANY)
total += reg("logined", PATHS_LOGINED)
total += reg("reseller.operator", PATHS_OPERATOR)
print(f"Done. {total} entries.")

8
setup.json Normal file
View File

@ -0,0 +1,8 @@
{
"name": "pcc",
"version": "0.1.0",
"description": "统一集群管理K8s/Slurm/Ray 集群生命周期、节点分配、动态维护、监控",
"packages": ["pcc", "pcc.k8s_plugin", "pcc.slurm_plugin", "pcc.ray_plugin"],
"install_requires": ["apppublic", "sqlor", "ahserver", "appbase", "rbac"],
"python_requires": ">=3.10"
}

View File

@ -0,0 +1,3 @@
# 向运行中集群动态添加节点
result = await add_cluster_node(request, params_kw)
return result if isinstance(result, dict) else {'widgettype': 'Error', 'options': {'title': '失败', 'message': str(result)}}

View File

@ -0,0 +1,3 @@
# cluster create
result = {'status': 'ok', 'message': 'cluster created'}
return result

View File

@ -0,0 +1,3 @@
# cluster delete
result = {'status': 'ok', 'message': 'cluster deleted'}
return result

View File

@ -0,0 +1,7 @@
# 部署集群:分配控制节点 + 算力节点 → 调用对应 plugin 部署
cluster_id = params_kw.get('cluster_id', '')
if not cluster_id:
return {'widgettype': 'Error', 'options': {'title': '失败', 'message': '缺少集群ID'}}
result = await deploy_cluster(request, cluster_id)
return result if isinstance(result, dict) else {'widgettype': 'Error', 'options': {'title': '失败', 'message': str(result)}}

View File

@ -0,0 +1,4 @@
# 销毁集群并回收所有节点
cluster_id = params_kw.get('cluster_id', '')
result = await destroy_cluster(request, cluster_id)
return result

View File

@ -0,0 +1,3 @@
# cluster_node create
result = {'status': 'ok', 'message': 'cluster_node created'}
return result

View File

@ -0,0 +1,3 @@
# cluster_node delete
result = {'status': 'ok', 'message': 'cluster_node deleted'}
return result

View File

@ -0,0 +1,3 @@
# cluster_node update
result = {'status': 'ok', 'message': 'cluster_node updated'}
return result

View File

@ -0,0 +1,3 @@
# 从集群移除节点
result = await remove_cluster_node(request, params_kw)
return result if isinstance(result, dict) else {'widgettype': 'Error', 'options': {'title': '失败', 'message': str(result)}}

View File

@ -0,0 +1,4 @@
# 启动集群
cluster_id = params_kw.get('cluster_id', '')
result = await start_cluster(request, cluster_id)
return result

View File

@ -0,0 +1,6 @@
# 获取集群运行状态
cluster_id = params_kw.get('cluster_id', '')
if not cluster_id:
return {'status': 'error', 'message': 'Missing cluster_id'}
result = await cluster_status(request, cluster_id)
return result

View File

@ -0,0 +1,4 @@
# 停止集群
cluster_id = params_kw.get('cluster_id', '')
result = await stop_cluster(request, cluster_id)
return result

View File

@ -0,0 +1,3 @@
# cluster update
result = {'status': 'ok', 'message': 'cluster updated'}
return result