""" 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)