pcc/build/lib/pcc/slurm_plugin/__init__.py

230 lines
9.6 KiB
Python

"""
slurm_plugin — Slurm 集群管理
- 控制节点: slurmctld + munge + NFS server
- 算力节点: slurmd + munge + NFS client
- 移除: drain node → scontrol delete → reset
- 状态: sinfo
"""
from pcc import ssh_exec
async def _node_info(env, node_id):
from sqlor.dbpools import DBPools
dbname = env.get_module_dbname('pccs')
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('compute_node', {'id': node_id})
return recs[0] if recs else None
async def _install_slurm_deps(node):
"""安装 Slurm 依赖 (munge + slurm)"""
script = (
"apt-get update -qq && "
"apt-get install -y -qq munge slurm-wlm slurm-client nfs-common nfs-kernel-server "
"&& systemctl enable munge && systemctl start munge"
)
return await ssh_exec(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root', script, timeout=300)
async def _setup_munge(control_node, compute_node):
"""复制 munge key 从控制节点到算力节点"""
cmd = f"scp -o StrictHostKeyChecking=no /etc/munge/munge.key {compute_node.ssh_user or 'root'}@{compute_node.ip_address}:/etc/munge/munge.key"
rc, _, err = await ssh_exec(control_node.ip_address, control_node.ssh_port or 22,
control_node.ssh_user or 'root', cmd, timeout=30)
if rc == 0:
await ssh_exec(compute_node.ip_address, compute_node.ssh_port or 22,
compute_node.ssh_user or 'root',
"chown munge:munge /etc/munge/munge.key && chmod 400 /etc/munge/munge.key && systemctl restart munge", timeout=30)
async def deploy_cluster(env, cluster_id, control_nodes, compute_nodes, config):
"""Slurm 集群部署:控制节点安装 slurmctld + munge"""
cluster_name = config.get('cluster_name', 'pccs-cluster')
results = {'control_nodes': {}, 'endpoint': ''}
for node in control_nodes:
# 1. 安装依赖
rc, out, err = await _install_slurm_deps(node)
if rc != 0:
results['control_nodes'][node.id] = {'error': f'install failed: {err[:200]}'}
continue
# 2. 生成 slurm.conf
cpu = node.cpu_cores or 1
mem = node.memory_gb or 4
conf = f"""ClusterName={cluster_name}
ControlMachine={node.ip_address}
SlurmUser=root
SlurmctldPort=6817
SlurmdPort=6818
AuthType=auth/munge
StateSaveLocation=/var/spool/slurmctld
SlurmdSpoolDir=/var/spool/slurmd
ReturnToService=1
SchedulerType=sched/backfill
SelectType=select/cons_tres
SelectTypeParameters=CR_Core
AccountingStorageType=accounting_storage/none
JobCompType=jobcomp/none
NodeName={node.name} CPUs={cpu} RealMemory={mem * 1024} State=UNKNOWN
PartitionName=debug Nodes={node.name} Default=YES MaxTime=INFINITE State=UP
"""
# 写配置到控制节点
write_cmd = f"cat > /etc/slurm/slurm.conf << 'SLURM_EOF'\n{conf}\nSLURM_EOF"
rc2, _, err2 = await ssh_exec(node.ip_address, node.ssh_port or 22,
node.ssh_user or 'root', write_cmd, timeout=30)
if rc2 != 0:
results['control_nodes'][node.id] = {'error': f'config write failed: {err2}'}
continue
# 3. 启动 slurmctld
await ssh_exec(node.ip_address, node.ssh_port or 22,
node.ssh_user or 'root',
"mkdir -p /var/spool/slurmctld /var/spool/slurmd && "
"systemctl enable slurmctld && systemctl start slurmctld", timeout=60)
results['control_nodes'][node.id] = {'status': 'ok', 'slurm_conf': conf}
results['endpoint'] = node.ip_address
async with DBPools().sqlorContext(env.get_module_dbname('pccs')) as sor:
await sor.U('cluster', {'id': cluster_id},
{'control_config': json.dumps(results)})
return results
async def add_node(env, cluster_id, node_id, role='compute'):
"""向 Slurm 集群添加算力节点:安装 slurmd + munge → 更新 slurm.conf"""
node = await _node_info(env, node_id)
if not node:
return {'status': 'error', 'message': f'Node {node_id} not found'}
# 获取控制节点
from sqlor.dbpools import DBPools
dbname = env.get_module_dbname('pccs')
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('cluster_node', {'cluster_id': cluster_id, 'role': 'control', 'status': 'active'})
if not recs:
return {'status': 'error', 'message': 'No active control node'}
cnode = await _node_info(env, recs[0].node_id)
if not cnode:
return {'status': 'error', 'message': 'Control node not found'}
# 1. 安装依赖
rc, _, err = await _install_slurm_deps(node)
if rc != 0:
return {'status': 'error', 'message': f'install failed: {err[:200]}'}
# 2. 复制 munge key
await _setup_munge(cnode, node)
# 3. 更新控制节点 slurm.conf 添加此节点
cpu = node.cpu_cores or 1
mem = node.memory_gb or 4
add_line = f"NodeName={node.name} CPUs={cpu} RealMemory={mem * 1024} State=UNKNOWN"
await ssh_exec(cnode.ip_address, cnode.ssh_port or 22, cnode.ssh_user or 'root',
f"echo '{add_line}' >> /etc/slurm/slurm.conf && scontrol reconfigure", timeout=30)
# 4. 启动 slurmd
await ssh_exec(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root',
"systemctl enable slurmd && systemctl start slurmd", timeout=60)
return {'status': 'ok', 'message': f'Node {node.name} joined Slurm cluster'}
async def remove_node(env, cluster_id, node_id):
"""从 Slurm 集群移除节点"""
node = await _node_info(env, node_id)
if not node:
return {'status': 'error', 'message': 'Node not found'}
# 获取控制节点
from sqlor.dbpools import DBPools
dbname = env.get_module_dbname('pccs')
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('cluster_node', {'cluster_id': cluster_id, 'role': 'control'})
if recs:
cnode = await _node_info(env, recs[0].node_id)
if cnode:
# drain → remove from slurm.conf → scontrol reconfigure
await ssh_exec(cnode.ip_address, cnode.ssh_port or 22, cnode.ssh_user or 'root',
f"scontrol update NodeName={node.name} State=DOWN Reason=removing && "
f"sed -i '/NodeName={node.name}/d' /etc/slurm/slurm.conf && "
"scontrol reconfigure", timeout=30)
# stop slurmd on the node
await ssh_exec(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root',
"systemctl stop slurmd && systemctl disable slurmd", timeout=30)
return {'status': 'ok', 'message': f'Node {node.name} removed from Slurm'}
async def cluster_status(env, cluster_id):
"""sinfo"""
from sqlor.dbpools import DBPools
dbname = env.get_module_dbname('pccs')
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('cluster_node', {'cluster_id': cluster_id, 'role': 'control', 'status': 'active'})
if not recs:
return {'error': 'No active control node'}
cnode = await _node_info(env, recs[0].node_id)
if not cnode:
return {'error': 'Control node not found'}
rc, out, _ = await ssh_exec(cnode.ip_address, cnode.ssh_port or 22,
cnode.ssh_user or 'root', 'sinfo', timeout=30)
return {'status': 'ok', 'sinfo': out if rc == 0 else 'sinfo failed'}
import json
from sqlor.dbpools import DBPools
async def allocate_unit(env, cluster_id, unit_name, cpu, memory, gpu=0):
"""Slurm 算力单元分配: 创建 partition + 关联节点"""
node = await _find_control_node(env, cluster_id)
if not node:
return {'status': 'error', 'message': 'No control node found'}
from pcc import ssh_exec as _ssh
# 创建 partition
cmd = f"scontrol create PartitionName={unit_name} MaxNodes=UNLIMITED Default=NO MaxTime=UNLIMITED State=UP"
rc, out, err = await _ssh(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root', cmd)
if rc != 0:
return {'status': 'error', 'message': f'scontrol failed: {err[:200]}'}
# 分配 node 到 partition (通过 cluster_node 表找该集群下可用的 compute 节点)
dbname = env.get_module_dbname('pccs')
async with DBPools().sqlorContext(dbname) as sor:
assigned = await sor.sqlExe(
"""SELECT cn.name FROM cluster_node cnn
JOIN compute_node cn ON cn.id = cnn.node_id
WHERE cnn.cluster_id=${cid}$ AND cnn.role='compute' AND cnn.status='active'
LIMIT 1""",
{'cid': cluster_id}
)
if assigned:
node_name = assigned[0].name
await _ssh(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root',
f"scontrol update NodeName={node_name} Partition={unit_name}")
return {'status': 'ok', 'message': f'Partition {unit_name} created'}
async def release_unit(env, cluster_id, unit_name):
"""Slurm 算力单元回收: 删除 partition"""
node = await _find_control_node(env, cluster_id)
if not node:
return {'status': 'error', 'message': 'No control node found'}
from pcc import ssh_exec as _ssh
await _ssh(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root',
f"scontrol delete PartitionName={unit_name}")
return {'status': 'ok', 'message': f'Partition {unit_name} deleted'}
async def _find_control_node(env, cluster_id):
from sqlor.dbpools import DBPools
dbname = env.get_module_dbname('pccs')
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('cluster_node', {'cluster_id': cluster_id, 'role': 'control', 'status': 'active'})
if not recs:
return None
return await _node_info(env, recs[0].node_id)