diff --git a/build/lib/build/lib/build/lib/pcpool/__init__.py b/build/lib/build/lib/build/lib/pcpool/__init__.py new file mode 100644 index 0000000..bc0a756 --- /dev/null +++ b/build/lib/build/lib/build/lib/pcpool/__init__.py @@ -0,0 +1,149 @@ +""" +pcpool — 算力中心算力池管理 + - 算力节点池 CRUD + - 节点注册/注销/心跳 + - 算力单元分配和回收 + - 池资源统计 +""" +import datetime +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +MODULE_NAME = 'pccs' + + +async def node_heartbeat(request, params_kw): + """节点心跳上报,更新 last_heartbeat""" + env = request._run_ns + node_id = params_kw.get('id') or params_kw.get('node_id', '') + if not node_id: + return {'status': 'error', 'message': 'Missing node id'} + dbname = env.get_module_dbname(MODULE_NAME) + now = datetime.datetime.now().isoformat() + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('compute_node', {'id': node_id}, + {'last_heartbeat': now, 'updated_at': now}) + return {'status': 'ok'} + + +async def allocate_nodes(request, params_kw): + """从算力池中分配节点给集群。 + params: pool_id, cluster_id, role (control/compute), count, cpu, memory, gpu + 返回分配的节点 ID 列表。""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + pool_id = params_kw.get('pool_id', '') + cluster_id = params_kw.get('cluster_id', '') + role = params_kw.get('role', 'compute') + count = int(params_kw.get('count', 1)) + req_cpu = int(params_kw.get('cpu', 0)) + req_mem = int(params_kw.get('memory', 0)) + req_gpu = int(params_kw.get('gpu', 0)) + + if not pool_id: + return {'status': 'error', 'message': 'Missing pool_id'} + + async with DBPools().sqlorContext(dbname) as sor: + # 查询可用节点 + sql = """SELECT * FROM compute_node + WHERE pool_id = ${pool_id}$ AND status = 'available' + ORDER BY cpu_cores DESC, memory_gb DESC + LIMIT ${limit}$""" + available = await sor.sqlExe(sql, {'pool_id': pool_id, 'limit': count}) + if len(available) < count: + return {'status': 'error', + 'message': f'可用节点不足: 需要{count}, 可用{len(available)}'} + + allocated = [] + for node in available[:count]: + await sor.U('compute_node', {'id': node.id}, + {'status': 'allocated', 'updated_at': datetime.datetime.now().isoformat()}) + # 记录分配关系 + cn_id = getID() + await sor.C('cluster_node', { + 'id': cn_id, 'cluster_id': cluster_id, + 'node_id': node.id, 'role': role, 'status': 'joining', + 'assigned_at': datetime.datetime.now().isoformat() + }) + allocated.append(node.id) + + # 更新池已分配资源 + if allocated: + await sor.sqlExe( + """UPDATE compute_pool + SET allocated_cpu = allocated_cpu + ${cpu}$, + allocated_memory_gb = allocated_memory_gb + ${mem}$, + allocated_gpu = allocated_gpu + ${gpu}$, + updated_at = ${now}$ + WHERE id = ${pool_id}$""", + {'cpu': req_cpu * len(allocated), 'mem': req_mem * len(allocated), + 'gpu': req_gpu * len(allocated), 'pool_id': pool_id, + 'now': datetime.datetime.now().isoformat()} + ) + + return {'status': 'ok', 'allocated_nodes': allocated} + + +async def release_nodes(request, params_kw): + """回收节点回算力池。 + params: node_ids (逗号分隔), pool_id""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + node_ids_str = params_kw.get('node_ids', '') + if not node_ids_str: + return {'status': 'error', 'message': 'Missing node_ids'} + node_ids = [n.strip() for n in node_ids_str.split(',') if n.strip()] + + pool_id = params_kw.get('pool_id', '') + now = datetime.datetime.now().isoformat() + + async with DBPools().sqlorContext(dbname) as sor: + for nid in node_ids: + await sor.U('compute_node', {'id': nid}, + {'status': 'available', 'updated_at': now}) + # 清除分配记录 + await sor.sqlExe( + "UPDATE cluster_node SET status='removed' WHERE node_id=${nid}$", + {'nid': nid}) + # 回收资源计数 + node_row = await sor.R('compute_node', {'id': nid}) + if node_row: + n = node_row[0] + await sor.sqlExe( + """UPDATE compute_pool + SET allocated_cpu = GREATEST(allocated_cpu - ${cpu}$, 0), + allocated_memory_gb = GREATEST(allocated_memory_gb - ${mem}$, 0), + allocated_gpu = GREATEST(allocated_gpu - ${gpu}$, 0), + updated_at = ${now}$ + WHERE id = ${pool_id}$""", + {'cpu': n.cpu_cores, 'mem': n.memory_gb, 'gpu': n.gpu_count, + 'pool_id': pool_id or n.pool_id, 'now': now}) + + return {'status': 'ok', 'released': len(node_ids)} + + +async def pool_stats(request, params_kw): + """获取池资源统计""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + pool_id = params_kw.get('pool_id', '') + async with DBPools().sqlorContext(dbname) as sor: + if pool_id: + pools = await sor.R('compute_pool', {'id': pool_id}) + else: + pools = await sor.R('compute_pool', {}) + if not pools: + return {'status': 'ok', 'data': []} + result = [] + for p in pools: + nodes = await sor.R('compute_node', {'pool_id': p.id}) + result.append({ + 'id': p.id, 'name': p.name, 'type': p.pool_type, + 'total_cpu': p.total_cpu, 'allocated_cpu': p.allocated_cpu, + 'total_memory_gb': p.total_memory_gb, 'allocated_memory_gb': p.allocated_memory_gb, + 'total_gpu': p.total_gpu, 'allocated_gpu': p.allocated_gpu, + 'node_count': len(nodes), + 'available_nodes': sum(1 for n in nodes if n.status == 'available'), + 'status': p.status + }) + return {'status': 'ok', 'data': result} diff --git a/build/lib/build/lib/pcpool/__init__.py b/build/lib/build/lib/pcpool/__init__.py new file mode 100644 index 0000000..bc0a756 --- /dev/null +++ b/build/lib/build/lib/pcpool/__init__.py @@ -0,0 +1,149 @@ +""" +pcpool — 算力中心算力池管理 + - 算力节点池 CRUD + - 节点注册/注销/心跳 + - 算力单元分配和回收 + - 池资源统计 +""" +import datetime +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +MODULE_NAME = 'pccs' + + +async def node_heartbeat(request, params_kw): + """节点心跳上报,更新 last_heartbeat""" + env = request._run_ns + node_id = params_kw.get('id') or params_kw.get('node_id', '') + if not node_id: + return {'status': 'error', 'message': 'Missing node id'} + dbname = env.get_module_dbname(MODULE_NAME) + now = datetime.datetime.now().isoformat() + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('compute_node', {'id': node_id}, + {'last_heartbeat': now, 'updated_at': now}) + return {'status': 'ok'} + + +async def allocate_nodes(request, params_kw): + """从算力池中分配节点给集群。 + params: pool_id, cluster_id, role (control/compute), count, cpu, memory, gpu + 返回分配的节点 ID 列表。""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + pool_id = params_kw.get('pool_id', '') + cluster_id = params_kw.get('cluster_id', '') + role = params_kw.get('role', 'compute') + count = int(params_kw.get('count', 1)) + req_cpu = int(params_kw.get('cpu', 0)) + req_mem = int(params_kw.get('memory', 0)) + req_gpu = int(params_kw.get('gpu', 0)) + + if not pool_id: + return {'status': 'error', 'message': 'Missing pool_id'} + + async with DBPools().sqlorContext(dbname) as sor: + # 查询可用节点 + sql = """SELECT * FROM compute_node + WHERE pool_id = ${pool_id}$ AND status = 'available' + ORDER BY cpu_cores DESC, memory_gb DESC + LIMIT ${limit}$""" + available = await sor.sqlExe(sql, {'pool_id': pool_id, 'limit': count}) + if len(available) < count: + return {'status': 'error', + 'message': f'可用节点不足: 需要{count}, 可用{len(available)}'} + + allocated = [] + for node in available[:count]: + await sor.U('compute_node', {'id': node.id}, + {'status': 'allocated', 'updated_at': datetime.datetime.now().isoformat()}) + # 记录分配关系 + cn_id = getID() + await sor.C('cluster_node', { + 'id': cn_id, 'cluster_id': cluster_id, + 'node_id': node.id, 'role': role, 'status': 'joining', + 'assigned_at': datetime.datetime.now().isoformat() + }) + allocated.append(node.id) + + # 更新池已分配资源 + if allocated: + await sor.sqlExe( + """UPDATE compute_pool + SET allocated_cpu = allocated_cpu + ${cpu}$, + allocated_memory_gb = allocated_memory_gb + ${mem}$, + allocated_gpu = allocated_gpu + ${gpu}$, + updated_at = ${now}$ + WHERE id = ${pool_id}$""", + {'cpu': req_cpu * len(allocated), 'mem': req_mem * len(allocated), + 'gpu': req_gpu * len(allocated), 'pool_id': pool_id, + 'now': datetime.datetime.now().isoformat()} + ) + + return {'status': 'ok', 'allocated_nodes': allocated} + + +async def release_nodes(request, params_kw): + """回收节点回算力池。 + params: node_ids (逗号分隔), pool_id""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + node_ids_str = params_kw.get('node_ids', '') + if not node_ids_str: + return {'status': 'error', 'message': 'Missing node_ids'} + node_ids = [n.strip() for n in node_ids_str.split(',') if n.strip()] + + pool_id = params_kw.get('pool_id', '') + now = datetime.datetime.now().isoformat() + + async with DBPools().sqlorContext(dbname) as sor: + for nid in node_ids: + await sor.U('compute_node', {'id': nid}, + {'status': 'available', 'updated_at': now}) + # 清除分配记录 + await sor.sqlExe( + "UPDATE cluster_node SET status='removed' WHERE node_id=${nid}$", + {'nid': nid}) + # 回收资源计数 + node_row = await sor.R('compute_node', {'id': nid}) + if node_row: + n = node_row[0] + await sor.sqlExe( + """UPDATE compute_pool + SET allocated_cpu = GREATEST(allocated_cpu - ${cpu}$, 0), + allocated_memory_gb = GREATEST(allocated_memory_gb - ${mem}$, 0), + allocated_gpu = GREATEST(allocated_gpu - ${gpu}$, 0), + updated_at = ${now}$ + WHERE id = ${pool_id}$""", + {'cpu': n.cpu_cores, 'mem': n.memory_gb, 'gpu': n.gpu_count, + 'pool_id': pool_id or n.pool_id, 'now': now}) + + return {'status': 'ok', 'released': len(node_ids)} + + +async def pool_stats(request, params_kw): + """获取池资源统计""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + pool_id = params_kw.get('pool_id', '') + async with DBPools().sqlorContext(dbname) as sor: + if pool_id: + pools = await sor.R('compute_pool', {'id': pool_id}) + else: + pools = await sor.R('compute_pool', {}) + if not pools: + return {'status': 'ok', 'data': []} + result = [] + for p in pools: + nodes = await sor.R('compute_node', {'pool_id': p.id}) + result.append({ + 'id': p.id, 'name': p.name, 'type': p.pool_type, + 'total_cpu': p.total_cpu, 'allocated_cpu': p.allocated_cpu, + 'total_memory_gb': p.total_memory_gb, 'allocated_memory_gb': p.allocated_memory_gb, + 'total_gpu': p.total_gpu, 'allocated_gpu': p.allocated_gpu, + 'node_count': len(nodes), + 'available_nodes': sum(1 for n in nodes if n.status == 'available'), + 'status': p.status + }) + return {'status': 'ok', 'data': result} diff --git a/build/lib/build/lib/scripts/load_path.py b/build/lib/build/lib/scripts/load_path.py new file mode 100644 index 0000000..fb17939 --- /dev/null +++ b/build/lib/build/lib/scripts/load_path.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""pcpool 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"/pcpool/api/{f}" for f in sorted(os.listdir(API_DIR)) if f.endswith(".dspy")] + +cruds = load_cruds() +apis = get_apis() + +PATHS_ANY = [ + f"/pcpool/menu.ui", +] +PATHS_LOGINED = [ + f"/pcpool", + f"/pcpool/index.ui", +] +for d in cruds: + PATHS_ANY.append(f"/pcpool/{d['alias']}") + PATHS_LOGINED.append(f"/pcpool/{d['alias']}/index.ui") + for act in ["get", "add", "update", "delete"]: + PATHS_LOGINED.append(f"/pcpool/{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.") diff --git a/build/lib/pcpool/__init__.py b/build/lib/pcpool/__init__.py new file mode 100644 index 0000000..bc0a756 --- /dev/null +++ b/build/lib/pcpool/__init__.py @@ -0,0 +1,149 @@ +""" +pcpool — 算力中心算力池管理 + - 算力节点池 CRUD + - 节点注册/注销/心跳 + - 算力单元分配和回收 + - 池资源统计 +""" +import datetime +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +MODULE_NAME = 'pccs' + + +async def node_heartbeat(request, params_kw): + """节点心跳上报,更新 last_heartbeat""" + env = request._run_ns + node_id = params_kw.get('id') or params_kw.get('node_id', '') + if not node_id: + return {'status': 'error', 'message': 'Missing node id'} + dbname = env.get_module_dbname(MODULE_NAME) + now = datetime.datetime.now().isoformat() + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('compute_node', {'id': node_id}, + {'last_heartbeat': now, 'updated_at': now}) + return {'status': 'ok'} + + +async def allocate_nodes(request, params_kw): + """从算力池中分配节点给集群。 + params: pool_id, cluster_id, role (control/compute), count, cpu, memory, gpu + 返回分配的节点 ID 列表。""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + pool_id = params_kw.get('pool_id', '') + cluster_id = params_kw.get('cluster_id', '') + role = params_kw.get('role', 'compute') + count = int(params_kw.get('count', 1)) + req_cpu = int(params_kw.get('cpu', 0)) + req_mem = int(params_kw.get('memory', 0)) + req_gpu = int(params_kw.get('gpu', 0)) + + if not pool_id: + return {'status': 'error', 'message': 'Missing pool_id'} + + async with DBPools().sqlorContext(dbname) as sor: + # 查询可用节点 + sql = """SELECT * FROM compute_node + WHERE pool_id = ${pool_id}$ AND status = 'available' + ORDER BY cpu_cores DESC, memory_gb DESC + LIMIT ${limit}$""" + available = await sor.sqlExe(sql, {'pool_id': pool_id, 'limit': count}) + if len(available) < count: + return {'status': 'error', + 'message': f'可用节点不足: 需要{count}, 可用{len(available)}'} + + allocated = [] + for node in available[:count]: + await sor.U('compute_node', {'id': node.id}, + {'status': 'allocated', 'updated_at': datetime.datetime.now().isoformat()}) + # 记录分配关系 + cn_id = getID() + await sor.C('cluster_node', { + 'id': cn_id, 'cluster_id': cluster_id, + 'node_id': node.id, 'role': role, 'status': 'joining', + 'assigned_at': datetime.datetime.now().isoformat() + }) + allocated.append(node.id) + + # 更新池已分配资源 + if allocated: + await sor.sqlExe( + """UPDATE compute_pool + SET allocated_cpu = allocated_cpu + ${cpu}$, + allocated_memory_gb = allocated_memory_gb + ${mem}$, + allocated_gpu = allocated_gpu + ${gpu}$, + updated_at = ${now}$ + WHERE id = ${pool_id}$""", + {'cpu': req_cpu * len(allocated), 'mem': req_mem * len(allocated), + 'gpu': req_gpu * len(allocated), 'pool_id': pool_id, + 'now': datetime.datetime.now().isoformat()} + ) + + return {'status': 'ok', 'allocated_nodes': allocated} + + +async def release_nodes(request, params_kw): + """回收节点回算力池。 + params: node_ids (逗号分隔), pool_id""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + node_ids_str = params_kw.get('node_ids', '') + if not node_ids_str: + return {'status': 'error', 'message': 'Missing node_ids'} + node_ids = [n.strip() for n in node_ids_str.split(',') if n.strip()] + + pool_id = params_kw.get('pool_id', '') + now = datetime.datetime.now().isoformat() + + async with DBPools().sqlorContext(dbname) as sor: + for nid in node_ids: + await sor.U('compute_node', {'id': nid}, + {'status': 'available', 'updated_at': now}) + # 清除分配记录 + await sor.sqlExe( + "UPDATE cluster_node SET status='removed' WHERE node_id=${nid}$", + {'nid': nid}) + # 回收资源计数 + node_row = await sor.R('compute_node', {'id': nid}) + if node_row: + n = node_row[0] + await sor.sqlExe( + """UPDATE compute_pool + SET allocated_cpu = GREATEST(allocated_cpu - ${cpu}$, 0), + allocated_memory_gb = GREATEST(allocated_memory_gb - ${mem}$, 0), + allocated_gpu = GREATEST(allocated_gpu - ${gpu}$, 0), + updated_at = ${now}$ + WHERE id = ${pool_id}$""", + {'cpu': n.cpu_cores, 'mem': n.memory_gb, 'gpu': n.gpu_count, + 'pool_id': pool_id or n.pool_id, 'now': now}) + + return {'status': 'ok', 'released': len(node_ids)} + + +async def pool_stats(request, params_kw): + """获取池资源统计""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + pool_id = params_kw.get('pool_id', '') + async with DBPools().sqlorContext(dbname) as sor: + if pool_id: + pools = await sor.R('compute_pool', {'id': pool_id}) + else: + pools = await sor.R('compute_pool', {}) + if not pools: + return {'status': 'ok', 'data': []} + result = [] + for p in pools: + nodes = await sor.R('compute_node', {'pool_id': p.id}) + result.append({ + 'id': p.id, 'name': p.name, 'type': p.pool_type, + 'total_cpu': p.total_cpu, 'allocated_cpu': p.allocated_cpu, + 'total_memory_gb': p.total_memory_gb, 'allocated_memory_gb': p.allocated_memory_gb, + 'total_gpu': p.total_gpu, 'allocated_gpu': p.allocated_gpu, + 'node_count': len(nodes), + 'available_nodes': sum(1 for n in nodes if n.status == 'available'), + 'status': p.status + }) + return {'status': 'ok', 'data': result} diff --git a/build/lib/scripts/load_path.py b/build/lib/scripts/load_path.py new file mode 100644 index 0000000..fb17939 --- /dev/null +++ b/build/lib/scripts/load_path.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""pcpool 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"/pcpool/api/{f}" for f in sorted(os.listdir(API_DIR)) if f.endswith(".dspy")] + +cruds = load_cruds() +apis = get_apis() + +PATHS_ANY = [ + f"/pcpool/menu.ui", +] +PATHS_LOGINED = [ + f"/pcpool", + f"/pcpool/index.ui", +] +for d in cruds: + PATHS_ANY.append(f"/pcpool/{d['alias']}") + PATHS_LOGINED.append(f"/pcpool/{d['alias']}/index.ui") + for act in ["get", "add", "update", "delete"]: + PATHS_LOGINED.append(f"/pcpool/{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.") diff --git a/i18n/en/msg.txt b/i18n/en/msg.txt new file mode 100644 index 0000000..18b6aea --- /dev/null +++ b/i18n/en/msg.txt @@ -0,0 +1,31 @@ +CPU核数: CPU Cores +GPU型号: GPU Model +GPU数: +GPU数量: GPU Count +IP地址: IP Address +SSH用户: SSH User +SSH端口: SSH Port +内存GB: Memory GB +创建时间: Created At +商户机构id: Reseller ID +已分配CPU: Allocated CPU +已分配GPU: Allocated GPUs +已分配内存GB: Allocated Memory GB +总CPU核数: Total CPU Cores +总GPU数: Total GPUs +总内存GB: Total Memory GB +所属算力池: Compute Pool +描述: Description +更新时间: Updated At +最后心跳: Last Heartbeat +池名称: Pool Name +状态: Status +状态(1=启用 0=停用): Status (1=Enabled 0=Disabled) +状态(available/allocated/maintenance/offline): +算力池: Compute Pool +算力节点: +节点名称: +节点类型: +节点类型(control/compute/storage): +集群类型: +集群类型(k8s/slurm/ray): diff --git a/i18n/zh/msg.txt b/i18n/zh/msg.txt new file mode 100644 index 0000000..ad60c7d --- /dev/null +++ b/i18n/zh/msg.txt @@ -0,0 +1,31 @@ +CPU核数: CPU核数 +GPU型号: GPU型号 +GPU数: GPU数 +GPU数量: GPU数量 +IP地址: IP地址 +SSH用户: SSH用户 +SSH端口: SSH端口 +内存GB: 内存GB +创建时间: 创建时间 +商户机构id: 商户机构id +已分配CPU: 已分配CPU +已分配GPU: 已分配GPU +已分配内存GB: 已分配内存GB +总CPU核数: 总CPU核数 +总GPU数: 总GPU数 +总内存GB: 总内存GB +所属算力池: 所属算力池 +描述: 描述 +更新时间: 更新时间 +最后心跳: 最后心跳 +池名称: 池名称 +状态: 状态 +状态(1=启用 0=停用): 状态(1=启用 0=停用) +状态(available/allocated/maintenance/offline): 状态(available/allocated/maintenance/offline) +算力池: 算力池 +算力节点: 算力节点 +节点名称: 节点名称 +节点类型: 节点类型 +节点类型(control/compute/storage): 节点类型(control/compute/storage) +集群类型: 集群类型 +集群类型(k8s/slurm/ray): 集群类型(k8s/slurm/ray) diff --git a/json/compute_node_list.json b/json/compute_node_list.json index 65b8592..94c1f69 100644 --- a/json/compute_node_list.json +++ b/json/compute_node_list.json @@ -47,13 +47,12 @@ "created_at", "updated_at" ], - "toolbar": { - "tools": [] - }, "binds": [], - "new_data_url": "{{entire_url('/pcpool/api/compute_node_create.dspy')}}", - "update_data_url": "{{entire_url('/pcpool/api/compute_node_update.dspy')}}", - "delete_data_url": "{{entire_url('/pcpool/api/compute_node_delete.dspy')}}", - "logined_userorgid": "resellerid" + "logined_userorgid": "resellerid", + "editable": { + "new_data_url": "{{entire_url('/pcpool/api/compute_node_create.dspy')}}", + "update_data_url": "{{entire_url('/pcpool/api/compute_node_update.dspy')}}", + "delete_data_url": "{{entire_url('/pcpool/api/compute_node_delete.dspy')}}" + } } } \ No newline at end of file diff --git a/json/compute_pool_list.json b/json/compute_pool_list.json index 1e8f6ac..ca4021b 100644 --- a/json/compute_pool_list.json +++ b/json/compute_pool_list.json @@ -12,30 +12,6 @@ "title": "集群类型", "width": 100 }, - "total_cpu": { - "title": "总CPU核数", - "width": 100 - }, - "total_memory_gb": { - "title": "总内存GB", - "width": 100 - }, - "total_gpu": { - "title": "总GPU数", - "width": 80 - }, - "allocated_cpu": { - "title": "已分配CPU", - "width": 100 - }, - "allocated_memory_gb": { - "title": "已分配内存GB", - "width": 120 - }, - "allocated_gpu": { - "title": "已分配GPU", - "width": 100 - }, "status": { "title": "状态", "width": 80 @@ -44,16 +20,27 @@ "editexclouded": [ "id", "resellerid", + "description", "created_at", "updated_at" ], - "toolbar": { - "tools": [] - }, "binds": [], - "new_data_url": "{{entire_url('/pcpool/api/compute_pool_create.dspy')}}", - "update_data_url": "{{entire_url('/pcpool/api/compute_pool_update.dspy')}}", - "delete_data_url": "{{entire_url('/pcpool/api/compute_pool_delete.dspy')}}", - "logined_userorgid": "resellerid" + "logined_userorgid": "resellerid", + "editable": { + "new_data_url": "{{entire_url('/pcpool/api/compute_pool_create.dspy')}}", + "update_data_url": "{{entire_url('/pcpool/api/compute_pool_update.dspy')}}", + "delete_data_url": "{{entire_url('/pcpool/api/compute_pool_delete.dspy')}}" + }, + "toolbar": { + "tools": [ + { + "name": "view_nodes", + "label": "查看节点", + "icon": "", + "type": "link", + "url": "/pcpool/compute_node_list/index.ui?pool_id={{id}}" + } + ] + } } } \ No newline at end of file diff --git a/models/compute_pool.json b/models/compute_pool.json index b47ba31..edb35ad 100644 --- a/models/compute_pool.json +++ b/models/compute_pool.json @@ -41,44 +41,7 @@ { "name": "description", "title": "描述", - "type": "str", - "length": 512 - }, - { - "name": "total_cpu", - "title": "总CPU核数", - "type": "int", - "default": 0 - }, - { - "name": "total_memory_gb", - "title": "总内存GB", - "type": "int", - "default": 0 - }, - { - "name": "total_gpu", - "title": "总GPU数", - "type": "int", - "default": 0 - }, - { - "name": "allocated_cpu", - "title": "已分配CPU", - "type": "int", - "default": 0 - }, - { - "name": "allocated_memory_gb", - "title": "已分配内存GB", - "type": "int", - "default": 0 - }, - { - "name": "allocated_gpu", - "title": "已分配GPU", - "type": "int", - "default": 0 + "type": "text" }, { "name": "status", diff --git a/pcpool.egg-info/PKG-INFO b/pcpool.egg-info/PKG-INFO new file mode 100644 index 0000000..0fa8908 --- /dev/null +++ b/pcpool.egg-info/PKG-INFO @@ -0,0 +1,9 @@ +Metadata-Version: 2.4 +Name: pcpool +Version: 0.1.0 +Summary: 算力中心算力池管理:节点池CRUD、节点注册/注销、算力单元分配回收 +Requires-Python: >=3.10 +Requires-Dist: apppublic +Requires-Dist: sqlor +Requires-Dist: ahserver +Requires-Dist: appbase diff --git a/pcpool.egg-info/SOURCES.txt b/pcpool.egg-info/SOURCES.txt new file mode 100644 index 0000000..f9ef40e --- /dev/null +++ b/pcpool.egg-info/SOURCES.txt @@ -0,0 +1,9 @@ +README.md +pyproject.toml +pcpool/__init__.py +pcpool.egg-info/PKG-INFO +pcpool.egg-info/SOURCES.txt +pcpool.egg-info/dependency_links.txt +pcpool.egg-info/requires.txt +pcpool.egg-info/top_level.txt +scripts/load_path.py \ No newline at end of file diff --git a/pcpool.egg-info/dependency_links.txt b/pcpool.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pcpool.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/pcpool.egg-info/requires.txt b/pcpool.egg-info/requires.txt new file mode 100644 index 0000000..dd1c806 --- /dev/null +++ b/pcpool.egg-info/requires.txt @@ -0,0 +1,4 @@ +apppublic +sqlor +ahserver +appbase diff --git a/pcpool.egg-info/top_level.txt b/pcpool.egg-info/top_level.txt new file mode 100644 index 0000000..d95657d --- /dev/null +++ b/pcpool.egg-info/top_level.txt @@ -0,0 +1,7 @@ +build +i18n +json +models +pcpool +scripts +wwwroot diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..66dbe39 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "pcpool" +version = "0.1.0" +description = "算力中心算力池管理:节点池CRUD、节点注册/注销、算力单元分配回收" +requires-python = ">=3.10" +dependencies = ["apppublic", "sqlor", "ahserver", "appbase"] + +[tool.setuptools.package-dir] +"pcpool" = "pcpool" + +[tool.setuptools.packages.find] +where = ["."] diff --git a/wwwroot/api/compute_pool_create.dspy b/wwwroot/api/compute_pool_create.dspy index 37db2d8..4aaec3f 100644 --- a/wwwroot/api/compute_pool_create.dspy +++ b/wwwroot/api/compute_pool_create.dspy @@ -1,3 +1,21 @@ -# compute_pool create -result = {'status': 'ok', 'message': 'compute_pool created'} -return result +ns = params_kw.copy() +for k,v in list(ns.items()): + if v == 'NaN' or v == 'null': + ns[k] = None + if k.endswith('_text'): + ns.pop(k, None) +id = params_kw.get('id','') +if not id or len(str(id)) > 32: + id = uuid() +ns['id'] = id +userorgid = await get_userorgid() +if not userorgid: + return {'widgettype':'Error','options':{'title':'Authorization Error','timeout':3,'cwidth':16,'cheight':9,'message':'Please login'}} +ns['resellerid'] = userorgid +ns['created_at'] = datetime.datetime.now().isoformat() +ns['updated_at'] = ns['created_at'] +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + await sor.C('compute_pool', ns) + return {'widgettype':'Message','options':{'cwidth':16,'cheight':9,'title':'Success','timeout':3,'message':'ok'}} diff --git a/wwwroot/api/compute_pool_delete.dspy b/wwwroot/api/compute_pool_delete.dspy index daf2cfb..c1c3a37 100644 --- a/wwwroot/api/compute_pool_delete.dspy +++ b/wwwroot/api/compute_pool_delete.dspy @@ -1,3 +1,8 @@ -# compute_pool delete -result = {'status': 'ok', 'message': 'compute_pool deleted'} -return result +ns = params_kw.copy() +if not ns.get('id'): + return {'widgettype':'Error','options':{'title':'Error','message':'missing id','cwidth':16,'cheight':9,'timeout':3}} +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + await sor.D('compute_pool', {'id': ns['id']}) + return {'widgettype':'Message','options':{'cwidth':16,'cheight':9,'title':'Success','timeout':3,'message':'ok'}} diff --git a/wwwroot/api/compute_pool_update.dspy b/wwwroot/api/compute_pool_update.dspy index 8894a38..5a124fc 100644 --- a/wwwroot/api/compute_pool_update.dspy +++ b/wwwroot/api/compute_pool_update.dspy @@ -1,3 +1,14 @@ -# compute_pool update -result = {'status': 'ok', 'message': 'compute_pool updated'} -return result +ns = params_kw.copy() +for k,v in list(ns.items()): + if v == 'NaN' or v == 'null': + ns[k] = None + if k.endswith('_text'): + ns.pop(k, None) +if not ns.get('id'): + return {'widgettype':'Error','options':{'title':'Error','message':'missing id','cwidth':16,'cheight':9,'timeout':3}} +ns['updated_at'] = datetime.datetime.now().isoformat() +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + await sor.U('compute_pool', ns) + return {'widgettype':'Message','options':{'cwidth':16,'cheight':9,'title':'Success','timeout':3,'message':'ok'}} diff --git a/wwwroot/api/node_heartbeat.dspy b/wwwroot/api/node_heartbeat.dspy index 17b26ca..0247e40 100644 --- a/wwwroot/api/node_heartbeat.dspy +++ b/wwwroot/api/node_heartbeat.dspy @@ -2,7 +2,6 @@ node_id = params_kw.get('id', '') if not node_id: return {'status': 'error', 'message': 'Missing node id'} -import datetime async with DBPools().sqlorContext('pccs') as sor: - await sor.U('compute_node', {'id': node_id}, {'last_heartbeat': datetime.datetime.now().isoformat(), 'updated_at': datetime.datetime.now().isoformat()}) + await sor.U('compute_node', {'id': node_id, 'last_heartbeat': datetime.datetime.now().isoformat(), 'updated_at': datetime.datetime.now().isoformat()}) return {'status': 'ok'} diff --git a/wwwroot/api/pool_stats.dspy b/wwwroot/api/pool_stats.dspy index fc01364..0587a5f 100644 --- a/wwwroot/api/pool_stats.dspy +++ b/wwwroot/api/pool_stats.dspy @@ -1,4 +1,50 @@ -# 算力池资源统计 -# params: pool_id (可选,不传则返回全部池) -result = await pool_stats(request, params_kw) -return result +env = request._run_ns +dbname = get_module_dbname('pcpool') +try: + async with DBPools().sqlorContext(dbname) as sor: + pools = await sor.R('compute_pool', {}) + total_pools = len(pools or []) + active = sum(1 for p in (pools or []) if str(getattr(p,'status','')) == '1') + total_cpu = 0; total_gpu = 0; total_mem = 0 + alloc_cpu = 0; alloc_gpu = 0; alloc_mem = 0 + for p in (pools or []): + nodes = await sor.R('compute_node', {'pool_id': p.id}) + for n in (nodes or []): + cpu = int(getattr(n,'cpu_cores',0) or 0) + gpu = int(getattr(n,'gpu_count',0) or 0) + mem = int(getattr(n,'memory_gb',0) or 0) + total_cpu += cpu; total_gpu += gpu; total_mem += mem + if getattr(n,'status','') == 'allocated': + alloc_cpu += cpu; alloc_gpu += gpu; alloc_mem += mem + return { + 'widgettype': 'HBox', + 'options': {'gap': '16px'}, + 'subwidgets': [ + {'widgettype': 'VBox', 'options': {'bgcolor': '#eff6ff', 'padding': '16px', 'width': '25%', 'border': '1px solid #dbeafe', 'borderRadius': '8px'}, + 'subwidgets': [ + {'widgettype': 'Text', 'options': {'otext': '算力池', 'cfontsize': 0.7, 'color': '#64748b'}}, + {'widgettype': 'Text', 'options': {'otext': str(total_pools) + ' 个', 'cfontsize': 2, 'fontWeight': 'bold', 'color': '#2563eb'}}, + {'widgettype': 'Text', 'options': {'otext': str(active) + ' 运行中', 'cfontsize': 0.7, 'color': '#94a3b8'}} + ]}, + {'widgettype': 'VBox', 'options': {'bgcolor': '#f5f3ff', 'padding': '16px', 'width': '25%', 'border': '1px solid #ede9fe', 'borderRadius': '8px'}, + 'subwidgets': [ + {'widgettype': 'Text', 'options': {'otext': 'CPU', 'cfontsize': 0.7, 'color': '#64748b'}}, + {'widgettype': 'Text', 'options': {'otext': str(total_cpu) + ' 核', 'cfontsize': 2, 'fontWeight': 'bold', 'color': '#7c3aed'}}, + {'widgettype': 'Text', 'options': {'otext': '已分配 ' + str(alloc_cpu) + ' 核', 'cfontsize': 0.7, 'color': '#94a3b8'}} + ]}, + {'widgettype': 'VBox', 'options': {'bgcolor': '#ecfdf5', 'padding': '16px', 'width': '25%', 'border': '1px solid #d1fae5', 'borderRadius': '8px'}, + 'subwidgets': [ + {'widgettype': 'Text', 'options': {'otext': 'GPU', 'cfontsize': 0.7, 'color': '#64748b'}}, + {'widgettype': 'Text', 'options': {'otext': str(total_gpu) + ' 个', 'cfontsize': 2, 'fontWeight': 'bold', 'color': '#059669'}}, + {'widgettype': 'Text', 'options': {'otext': '已分配 ' + str(alloc_gpu) + ' 个', 'cfontsize': 0.7, 'color': '#94a3b8'}} + ]}, + {'widgettype': 'VBox', 'options': {'bgcolor': '#fff7ed', 'padding': '16px', 'width': '25%', 'border': '1px solid #ffedd5', 'borderRadius': '8px'}, + 'subwidgets': [ + {'widgettype': 'Text', 'options': {'otext': '内存', 'cfontsize': 0.7, 'color': '#64748b'}}, + {'widgettype': 'Text', 'options': {'otext': str(total_mem) + ' GB', 'cfontsize': 2, 'fontWeight': 'bold', 'color': '#ea580c'}}, + {'widgettype': 'Text', 'options': {'otext': '已分配 ' + str(alloc_mem) + ' GB', 'cfontsize': 0.7, 'color': '#94a3b8'}} + ]} + ] + } +except: + return {'widgettype': 'Text', 'options': {'otext': '加载失败', 'color': '#dc2626'}} diff --git a/wwwroot/compute_node_list/add_compute_node.dspy b/wwwroot/compute_node_list/add_compute_node.dspy new file mode 100644 index 0000000..f8e4d7d --- /dev/null +++ b/wwwroot/compute_node_list/add_compute_node.dspy @@ -0,0 +1,57 @@ + +ns = params_kw.copy() +for k,v in ns.items(): + if v == 'NaN' or v == 'null': + ns[k] = None +id = params_kw.id +if not id or len(id) > 32: + id = uuid() +ns['id'] = id + + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['resellerid'] = userorgid + +for k in list(ns.keys()): + if k.endswith('_at') or k.endswith('_time') or k == 'last_heartbeat': + v = ns.get(k, '') + if v == '' or v is None or v == 'None': + ns[k] = None +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('compute_node', ns.copy()) + return { + "widgettype":"Message", + "options":{ + "user_data":ns, + "cwidth":16, + "cheight":9, + "title":"Add Success", + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Add Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/compute_node_list/delete_compute_node.dspy b/wwwroot/compute_node_list/delete_compute_node.dspy new file mode 100644 index 0000000..3a41906 --- /dev/null +++ b/wwwroot/compute_node_list/delete_compute_node.dspy @@ -0,0 +1,47 @@ + +ns = { + 'id':params_kw['id'], +} + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['resellerid'] = userorgid + +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('compute_node', ns) + debug('delete success'); + return { + "widgettype":"Message", + "options":{ + "title":"Delete Success", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"ok" + } + } + +debug('Delete failed'); +return { + "widgettype":"Error", + "options":{ + "title":"Delete Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/compute_node_list/get_compute_node.dspy b/wwwroot/compute_node_list/get_compute_node.dspy new file mode 100644 index 0000000..1df6b8d --- /dev/null +++ b/wwwroot/compute_node_list/get_compute_node.dspy @@ -0,0 +1,158 @@ + +ns = params_kw.copy() + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['resellerid'] = userorgid +ns['userorgid'] = userorgid + +debug(f'get_compute_node.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + ns['sort'] = 'id' + + +sql = '''select a.*, b.pool_id_text, c.node_type_text, d.status_text +from (select * from compute_node where 1=1 [[filterstr]]) a left join (select id as pool_id, + name as pool_id_text from compute_pool where 1 = 1) b on a.pool_id = b.pool_id left join (select k as node_type, + v as node_type_text from appcodes_kv where parentid='node_type') c on a.node_type = c.node_type left join (select k as status, + v as status_text from appcodes_kv where parentid='node_status') d on a.status = d.status''' + +filterjson = params_kw.get('data_filter') +if not filterjson: + fields = [ f['name'] for f in [ + { + "name": "id", + "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": "ip_address", + "title": "IP地址", + "type": "str", + "length": 64, + "nullable": "no" + }, + { + "name": "ssh_port", + "title": "SSH端口", + "type": "int", + "default": 22 + }, + { + "name": "ssh_user", + "title": "SSH用户", + "type": "str", + "length": 64 + }, + { + "name": "cpu_cores", + "title": "CPU核数", + "type": "int", + "default": 0 + }, + { + "name": "memory_gb", + "title": "内存GB", + "type": "int", + "default": 0 + }, + { + "name": "gpu_count", + "title": "GPU数量", + "type": "int", + "default": 0 + }, + { + "name": "gpu_type", + "title": "GPU型号", + "type": "str", + "length": 64 + }, + { + "name": "node_type", + "title": "节点类型(control/compute/storage)", + "type": "char", + "length": 16 + }, + { + "name": "status", + "title": "状态(available/allocated/maintenance/offline)", + "type": "char", + "length": 16, + "default": "available" + }, + { + "name": "last_heartbeat", + "title": "最后心跳", + "type": "timestamp" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "nullable": "no" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "nullable": "no" + } +] ] + filterjson = default_filterjson(fields, ns) +filterdic = ns.copy() +filterdic['filterstr'] = '' +filterdic['userorgid'] = '${userorgid}$' +filterdic['userid'] = '${userid}$' +if filterjson: + dbf = DBFilter(filterjson) + conds = dbf.gen(ns) + if conds: + ns.update(dbf.consts) + conds = f' and {conds}' + filterdic['filterstr'] = conds +ac = ArgsConvert('[[', ']]') +vars = ac.findAllVariables(sql) +NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' } +filterdic.update(NameSpace) +sql = ac.convert(sql, filterdic) + +debug(f'{sql=}') +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + r = await sor.sqlPaging(sql, ns) + return r +return { + "total":0, + "rows":[] +} \ No newline at end of file diff --git a/wwwroot/compute_node_list/index.ui b/wwwroot/compute_node_list/index.ui new file mode 100644 index 0000000..7f2e8b4 --- /dev/null +++ b/wwwroot/compute_node_list/index.ui @@ -0,0 +1,280 @@ + +{ + "id":"compute_node_tbl", + "widgettype":"Tabular", + "options":{ + "width":"100%", + "height":"100%", + + + "title":"算力节点", + + + + + "css":"card", + + + "editable":{ + + "new_data_url":"{{entire_url('add_compute_node.dspy')}}", + + + "delete_data_url":"{{entire_url('delete_compute_node.dspy')}}", + + + "update_data_url":"{{entire_url('update_compute_node.dspy')}}" + + }, + + + "data_url":"{{entire_url('./get_compute_node.dspy')}}", + + "data_method":"GET", + "data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options":{ + + + + "browserfields": { + "pool_id": { + "title": "所属算力池", + "width": 150 + }, + "name": { + "title": "节点名称", + "width": 150 + }, + "ip_address": { + "title": "IP地址", + "width": 140 + }, + "cpu_cores": { + "title": "CPU核数", + "width": 80 + }, + "memory_gb": { + "title": "内存GB", + "width": 80 + }, + "gpu_count": { + "title": "GPU数", + "width": 70 + }, + "gpu_type": { + "title": "GPU型号", + "width": 100 + }, + "node_type": { + "title": "节点类型", + "width": 100 + }, + "status": { + "title": "状态", + "width": 100 + } +}, + + + "editexclouded":[ + "id", + "resellerid", + "created_at", + "updated_at" +], + + "fields":[ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "id" + }, + { + "name": "pool_id", + "title": "所属算力池", + "type": "str", + "length": 32, + "label": "所属算力池", + "uitype": "code", + "valueField": "pool_id", + "textField": "pool_id_text", + "params": { + "dbname": "{{get_module_dbname('pcpool')}}", + "table": "compute_pool", + "tblvalue": "id", + "tbltext": "name", + "valueField": "pool_id", + "textField": "pool_id_text" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "name", + "title": "节点名称", + "type": "str", + "length": 128, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "节点名称" + }, + { + "name": "ip_address", + "title": "IP地址", + "type": "str", + "length": 64, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "IP地址" + }, + { + "name": "ssh_port", + "title": "SSH端口", + "type": "int", + "default": 22, + "length": 0, + "uitype": "int", + "datatype": "int", + "label": "SSH端口" + }, + { + "name": "ssh_user", + "title": "SSH用户", + "type": "str", + "length": 64, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "SSH用户" + }, + { + "name": "cpu_cores", + "title": "CPU核数", + "type": "int", + "default": 0, + "length": 0, + "uitype": "int", + "datatype": "int", + "label": "CPU核数" + }, + { + "name": "memory_gb", + "title": "内存GB", + "type": "int", + "default": 0, + "length": 0, + "uitype": "int", + "datatype": "int", + "label": "内存GB" + }, + { + "name": "gpu_count", + "title": "GPU数量", + "type": "int", + "default": 0, + "length": 0, + "uitype": "int", + "datatype": "int", + "label": "GPU数量" + }, + { + "name": "gpu_type", + "title": "GPU型号", + "type": "str", + "length": 64, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "GPU型号" + }, + { + "name": "node_type", + "title": "节点类型(control/compute/storage)", + "type": "char", + "length": 16, + "label": "节点类型(control/compute/storage)", + "uitype": "code", + "valueField": "node_type", + "textField": "node_type_text", + "params": { + "dbname": "{{get_module_dbname('pcpool')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "node_type", + "textField": "node_type_text", + "cond": "parentid='node_type'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "status", + "title": "状态(available/allocated/maintenance/offline)", + "type": "char", + "length": 16, + "default": "available", + "label": "状态(available/allocated/maintenance/offline)", + "uitype": "code", + "valueField": "status", + "textField": "status_text", + "params": { + "dbname": "{{get_module_dbname('pcpool')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "status", + "textField": "status_text", + "cond": "parentid='node_status'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "last_heartbeat", + "title": "最后心跳", + "type": "timestamp", + "length": 0, + "uitype": "str", + "datatype": "timestamp", + "label": "最后心跳" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "nullable": "no", + "length": 0, + "uitype": "str", + "datatype": "timestamp", + "label": "创建时间" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "nullable": "no", + "length": 0, + "uitype": "str", + "datatype": "timestamp", + "label": "更新时间" + } +] + }, + + + + "page_rows":160, + "cache_limit":5 + } + + ,"binds":[] + +} \ No newline at end of file diff --git a/wwwroot/compute_node_list/update_compute_node.dspy b/wwwroot/compute_node_list/update_compute_node.dspy new file mode 100644 index 0000000..118ccdd --- /dev/null +++ b/wwwroot/compute_node_list/update_compute_node.dspy @@ -0,0 +1,75 @@ + +ns = params_kw.copy() +for k,v in ns.items(): + if v == 'NaN' or v == 'null': + ns[k] = None + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['resellerid'] = userorgid + + + +for k in list(ns.keys()): + if k.endswith('_at') or k.endswith('_time') or k == 'last_heartbeat': + v = ns.get(k, '') + if v == '' or v is None or v == 'None': + ns[k] = None +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + + ns1 = { + + "resellerid": userorgid, + + + "id": params_kw.id + } + recs = await sor.R('compute_node', ns1) + if len(recs) < 1: + return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"Record no exist or with wrong ownership" + } + } + + r = await sor.U('compute_node', ns) + debug('update success'); + return { + "widgettype":"Message", + "options":{ + "title":"Update Success", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/compute_pool_list/add_compute_pool.dspy b/wwwroot/compute_pool_list/add_compute_pool.dspy new file mode 100644 index 0000000..e8fe78e --- /dev/null +++ b/wwwroot/compute_pool_list/add_compute_pool.dspy @@ -0,0 +1,52 @@ + +ns = params_kw.copy() +for k,v in ns.items(): + if v == 'NaN' or v == 'null': + ns[k] = None +id = params_kw.id +if not id or len(id) > 32: + id = uuid() +ns['id'] = id + + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['resellerid'] = userorgid + +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('compute_pool', ns.copy()) + return { + "widgettype":"Message", + "options":{ + "user_data":ns, + "cwidth":16, + "cheight":9, + "title":"Add Success", + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Add Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/compute_pool_list/delete_compute_pool.dspy b/wwwroot/compute_pool_list/delete_compute_pool.dspy new file mode 100644 index 0000000..fc286d9 --- /dev/null +++ b/wwwroot/compute_pool_list/delete_compute_pool.dspy @@ -0,0 +1,47 @@ + +ns = { + 'id':params_kw['id'], +} + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['resellerid'] = userorgid + +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('compute_pool', ns) + debug('delete success'); + return { + "widgettype":"Message", + "options":{ + "title":"Delete Success", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"ok" + } + } + +debug('Delete failed'); +return { + "widgettype":"Error", + "options":{ + "title":"Delete Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/compute_pool_list/get_compute_pool.dspy b/wwwroot/compute_pool_list/get_compute_pool.dspy new file mode 100644 index 0000000..dfce323 --- /dev/null +++ b/wwwroot/compute_pool_list/get_compute_pool.dspy @@ -0,0 +1,116 @@ + +ns = params_kw.copy() + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['resellerid'] = userorgid +ns['userorgid'] = userorgid + +debug(f'get_compute_pool.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + ns['sort'] = 'id' + + +sql = '''select a.*, b.pool_type_text, c.status_text +from (select * from compute_pool where 1=1 [[filterstr]]) a left join (select k as pool_type, + v as pool_type_text from appcodes_kv where parentid='cluster_type') b on a.pool_type = b.pool_type left join (select k as status, + v as status_text from appcodes_kv where parentid='product_status') c on a.status = c.status''' + +filterjson = params_kw.get('data_filter') +if not filterjson: + fields = [ f['name'] for f in [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "resellerid", + "title": "商户机构id", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "name", + "title": "池名称", + "type": "str", + "length": 128, + "nullable": "no" + }, + { + "name": "pool_type", + "title": "集群类型(k8s/slurm/ray)", + "type": "char", + "length": 16, + "nullable": "no" + }, + { + "name": "description", + "title": "描述", + "type": "text" + }, + { + "name": "status", + "title": "状态(1=启用 0=停用)", + "type": "char", + "length": 1, + "default": "1" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "nullable": "no" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "nullable": "no" + } +] ] + filterjson = default_filterjson(fields, ns) +filterdic = ns.copy() +filterdic['filterstr'] = '' +filterdic['userorgid'] = '${userorgid}$' +filterdic['userid'] = '${userid}$' +if filterjson: + dbf = DBFilter(filterjson) + conds = dbf.gen(ns) + if conds: + ns.update(dbf.consts) + conds = f' and {conds}' + filterdic['filterstr'] = conds +ac = ArgsConvert('[[', ']]') +vars = ac.findAllVariables(sql) +NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' } +filterdic.update(NameSpace) +sql = ac.convert(sql, filterdic) + +debug(f'{sql=}') +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + r = await sor.sqlPaging(sql, ns) + return r +return { + "total":0, + "rows":[] +} \ No newline at end of file diff --git a/wwwroot/compute_pool_list/index.ui b/wwwroot/compute_pool_list/index.ui new file mode 100644 index 0000000..0919cc1 --- /dev/null +++ b/wwwroot/compute_pool_list/index.ui @@ -0,0 +1,191 @@ + +{ + "id":"compute_pool_tbl", + "widgettype":"Tabular", + "options":{ + "width":"100%", + "height":"100%", + + + "title":"算力池", + + + + + "toolbar":{ + "tools": [ + { + "name": "view_nodes", + "label": "查看节点", + "icon": "", + "type": "link", + "url": "/pcpool/compute_node_list/index.ui?pool_id={{id}}" + } + ] +}, + + "css":"card", + + + "editable":{ + + "new_data_url":"{{entire_url('add_compute_pool.dspy')}}", + + + "delete_data_url":"{{entire_url('delete_compute_pool.dspy')}}", + + + "update_data_url":"{{entire_url('update_compute_pool.dspy')}}" + + }, + + + "data_url":"{{entire_url('./get_compute_pool.dspy')}}", + + "data_method":"GET", + "data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options":{ + + + + "browserfields": { + "name": { + "title": "池名称", + "width": 150 + }, + "pool_type": { + "title": "集群类型", + "width": 100 + }, + "status": { + "title": "状态", + "width": 80 + } +}, + + + "editexclouded":[ + "id", + "resellerid", + "description", + "created_at", + "updated_at" +], + + "fields":[ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "id" + }, + { + "name": "resellerid", + "title": "商户机构id", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "商户机构id" + }, + { + "name": "name", + "title": "池名称", + "type": "str", + "length": 128, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "池名称" + }, + { + "name": "pool_type", + "title": "集群类型(k8s/slurm/ray)", + "type": "char", + "length": 16, + "nullable": "no", + "label": "集群类型(k8s/slurm/ray)", + "uitype": "code", + "valueField": "pool_type", + "textField": "pool_type_text", + "params": { + "dbname": "{{get_module_dbname('pcpool')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "pool_type", + "textField": "pool_type_text", + "cond": "parentid='cluster_type'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "description", + "title": "描述", + "type": "text", + "length": 0, + "uitype": "text", + "datatype": "text", + "label": "描述" + }, + { + "name": "status", + "title": "状态(1=启用 0=停用)", + "type": "char", + "length": 1, + "default": "1", + "label": "状态(1=启用 0=停用)", + "uitype": "code", + "valueField": "status", + "textField": "status_text", + "params": { + "dbname": "{{get_module_dbname('pcpool')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "status", + "textField": "status_text", + "cond": "parentid='product_status'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "nullable": "no", + "length": 0, + "uitype": "str", + "datatype": "timestamp", + "label": "创建时间" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "nullable": "no", + "length": 0, + "uitype": "str", + "datatype": "timestamp", + "label": "更新时间" + } +] + }, + + + + "page_rows":160, + "cache_limit":5 + } + + ,"binds":[] + +} \ No newline at end of file diff --git a/wwwroot/compute_pool_list/update_compute_pool.dspy b/wwwroot/compute_pool_list/update_compute_pool.dspy new file mode 100644 index 0000000..a9a88ce --- /dev/null +++ b/wwwroot/compute_pool_list/update_compute_pool.dspy @@ -0,0 +1,70 @@ + +ns = params_kw.copy() +for k,v in ns.items(): + if v == 'NaN' or v == 'null': + ns[k] = None + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['resellerid'] = userorgid + + + +db = DBPools() +dbname = get_module_dbname('pcpool') +async with db.sqlorContext(dbname) as sor: + + ns1 = { + + "resellerid": userorgid, + + + "id": params_kw.id + } + recs = await sor.R('compute_pool', ns1) + if len(recs) < 1: + return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"Record no exist or with wrong ownership" + } + } + + r = await sor.U('compute_pool', ns) + debug('update success'); + return { + "widgettype":"Message", + "options":{ + "title":"Update Success", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file