From c2023174abc8e7231534127970c2d820d5803c39 Mon Sep 17 00:00:00 2001 From: pccs Date: Wed, 12 Aug 2026 19:35:32 +0800 Subject: [PATCH] feat: menu/i18n/CRUD/permission/stats/model fixes --- .../lib/build/lib/storage_mgr/__init__.py | 208 ++++++++++++++++ .../build/lib/build/lib/scripts/load_path.py | 70 ++++++ .../lib/build/lib/storage_mgr/__init__.py | 208 ++++++++++++++++ .../build/lib/build/lib/scripts/load_path.py | 70 ++++++ .../lib/build/lib/storage_mgr/__init__.py | 208 ++++++++++++++++ .../build/lib/build/lib/scripts/load_path.py | 70 ++++++ .../lib/build/lib/storage_mgr/__init__.py | 208 ++++++++++++++++ build/lib/build/lib/scripts/load_path.py | 70 ++++++ build/lib/build/lib/storage_mgr/__init__.py | 208 ++++++++++++++++ build/lib/scripts/load_path.py | 70 ++++++ build/lib/storage_mgr/__init__.py | 208 ++++++++++++++++ i18n/en/msg.txt | 36 +++ i18n/zh/msg.txt | 36 +++ json/storage_export_list.json | 50 +++- json/storage_mount_list.json | 46 +++- json/storage_server_list.json | 42 +++- models/storage_server.json | 98 ++++++-- pyproject.toml | 16 ++ storage_mgr.egg-info/PKG-INFO | 9 + storage_mgr.egg-info/SOURCES.txt | 9 + storage_mgr.egg-info/dependency_links.txt | 1 + storage_mgr.egg-info/requires.txt | 4 + storage_mgr.egg-info/top_level.txt | 7 + storage_mgr/__init__.py | 20 ++ wwwroot/api/storage_mount_exec.dspy | 8 +- wwwroot/api/storage_quota_set.dspy | 8 +- wwwroot/api/storage_stats.dspy | 18 ++ wwwroot/api/storage_umount_exec.dspy | 8 +- .../add_storage_export.dspy | 57 +++++ .../delete_storage_export.dspy | 47 ++++ .../get_storage_export.dspy | 138 ++++++++++ wwwroot/storage_export_list/index.ui | 232 +++++++++++++++++ .../update_storage_export.dspy | 75 ++++++ .../storage_mount_list/add_storage_mount.dspy | 57 +++++ .../delete_storage_mount.dspy | 47 ++++ .../storage_mount_list/get_storage_mount.dspy | 137 ++++++++++ wwwroot/storage_mount_list/index.ui | 235 ++++++++++++++++++ .../update_storage_mount.dspy | 75 ++++++ .../add_storage_server.dspy | 57 +++++ .../delete_storage_server.dspy | 47 ++++ .../get_storage_server.dspy | 118 +++++++++ wwwroot/storage_server_list/index.ui | 184 ++++++++++++++ .../update_storage_server.dspy | 75 ++++++ 43 files changed, 3529 insertions(+), 66 deletions(-) create mode 100644 build/lib/build/lib/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py create mode 100644 build/lib/build/lib/build/lib/build/lib/build/lib/scripts/load_path.py create mode 100644 build/lib/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py create mode 100644 build/lib/build/lib/build/lib/build/lib/scripts/load_path.py create mode 100644 build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py create mode 100644 build/lib/build/lib/build/lib/scripts/load_path.py create mode 100644 build/lib/build/lib/build/lib/storage_mgr/__init__.py create mode 100644 build/lib/build/lib/scripts/load_path.py create mode 100644 build/lib/build/lib/storage_mgr/__init__.py create mode 100644 build/lib/scripts/load_path.py create mode 100644 build/lib/storage_mgr/__init__.py create mode 100644 i18n/en/msg.txt create mode 100644 i18n/zh/msg.txt create mode 100644 pyproject.toml create mode 100644 storage_mgr.egg-info/PKG-INFO create mode 100644 storage_mgr.egg-info/SOURCES.txt create mode 100644 storage_mgr.egg-info/dependency_links.txt create mode 100644 storage_mgr.egg-info/requires.txt create mode 100644 storage_mgr.egg-info/top_level.txt create mode 100644 wwwroot/api/storage_stats.dspy create mode 100644 wwwroot/storage_export_list/add_storage_export.dspy create mode 100644 wwwroot/storage_export_list/delete_storage_export.dspy create mode 100644 wwwroot/storage_export_list/get_storage_export.dspy create mode 100644 wwwroot/storage_export_list/index.ui create mode 100644 wwwroot/storage_export_list/update_storage_export.dspy create mode 100644 wwwroot/storage_mount_list/add_storage_mount.dspy create mode 100644 wwwroot/storage_mount_list/delete_storage_mount.dspy create mode 100644 wwwroot/storage_mount_list/get_storage_mount.dspy create mode 100644 wwwroot/storage_mount_list/index.ui create mode 100644 wwwroot/storage_mount_list/update_storage_mount.dspy create mode 100644 wwwroot/storage_server_list/add_storage_server.dspy create mode 100644 wwwroot/storage_server_list/delete_storage_server.dspy create mode 100644 wwwroot/storage_server_list/get_storage_server.dspy create mode 100644 wwwroot/storage_server_list/index.ui create mode 100644 wwwroot/storage_server_list/update_storage_server.dspy diff --git a/build/lib/build/lib/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py b/build/lib/build/lib/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py new file mode 100644 index 0000000..f5ece0f --- /dev/null +++ b/build/lib/build/lib/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py @@ -0,0 +1,208 @@ +""" +storage_mgr — 共享存储管理 + +- NFS 服务器注册 +- 存储导出管理 +- 节点挂载/卸载 +- 存储配额控制 +""" +import datetime, json, asyncio, subprocess +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +MODULE_NAME = 'pccs' + + +async def ssh_exec(host, port, user, cmd, timeout=120): + """异步 SSH 远程执行""" + ssh_cmd = [ + 'ssh', '-o', 'StrictHostKeyChecking=no', + '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', + '-p', str(port), user + '@' + host, cmd + ] + proc = await asyncio.create_subprocess_exec( + *ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout) + except asyncio.TimeoutError: + proc.kill() + return -1, '', 'SSH timeout after ' + str(timeout) + 's' + return proc.returncode, stdout.decode(), stderr.decode() + + +async def storage_mount_exec(request, params_kw): + """ + 将存储导出挂载到目标节点。 + params: mount_id (storage_mount 记录 ID, 含 export_id + node_id + mount_point) + 流程: + 1. 读取 storage_mount 记录 → export_id, node_id, mount_point + 2. 读取 storage_export → server_id, export_path + 3. 读取 storage_server → endpoint + 4. 读取 compute_node → ip_address, ssh_port, ssh_user + 5. SSH 到目标节点执行 mount + 6. 更新 storage_mount 状态 + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + # 1. 读取挂载记录 + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + # 2. 读取导出 + exports = await sor.R('storage_export', {'id': mnt.export_id}) + if not exports: + return {'status': 'error', 'message': 'Export not found'} + exp = exports[0] + + # 3. 读取服务器 + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'error', 'message': 'Storage server not found'} + srv = servers[0] + + # 4. 读取目标节点 + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Target node not found'} + node = nodes[0] + + mount_point = mnt.mount_point + mount_opts = mnt.mount_options or 'nfsvers=4.2,hard,timeo=600,retrans=3' + nfs_source = srv.endpoint + ':' + exp.export_path + + # 5. SSH 执行挂载 + cmds = [ + 'mkdir -p ' + mount_point, + 'mountpoint -q ' + mount_point + ' && umount -l ' + mount_point + ' || true', + 'mount -t nfs4 -o ' + mount_opts + ' ' + nfs_source + ' ' + mount_point, + ] + for cmd in cmds: + rc, out, err = await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=60) + if rc != 0: + # 更新状态为失败 + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'failed', + 'updated_at': datetime.datetime.now().isoformat()}) + return {'status': 'error', 'message': 'Mount failed: ' + err[:200], 'cmd': cmd} + + # 写入 fstab 持久化 + fstab_line = nfs_source + ' ' + mount_point + ' nfs4 ' + mount_opts + ' 0 0' + await ssh_exec(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root', + "grep -q '" + mount_point + "' /etc/fstab || echo '" + fstab_line + "' >> /etc/fstab", + timeout=10) + + # 6. 更新状态 + now = datetime.datetime.now().isoformat() + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'mounted', 'mounted_at': now, + 'updated_at': now}) + + return {'status': 'ok', 'message': 'Mounted ' + nfs_source + ' → ' + mount_point} + + +async def storage_umount_exec(request, params_kw): + """从目标节点卸载存储""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Node not found'} + node = nodes[0] + + # 卸载 + 清理 fstab + cmds = [ + 'umount -l ' + mnt.mount_point + ' 2>/dev/null || true', + "sed -i '\\|" + mnt.mount_point + "|d' /etc/fstab", + ] + for cmd in cmds: + await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=30) + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'unmounted', + 'updated_at': datetime.datetime.now().isoformat()}) + + return {'status': 'ok', 'message': 'Unmounted ' + mnt.mount_point} + + +async def storage_quota_set(request, params_kw): + """ + 设置存储导出配额。 + params: export_id, size_limit_gb + 流程: + 1. 更新 storage_export.size_limit_gb + 2. 如果 NFS 服务器支持, SSH 到服务器设置 NFS quota + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + export_id = params_kw.get('export_id', '') + size_limit_gb = int(params_kw.get('size_limit_gb', 0)) + + if not export_id: + return {'status': 'error', 'message': 'Missing export_id'} + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_export', {'id': export_id}, + {'size_limit_gb': size_limit_gb, + 'updated_at': datetime.datetime.now().isoformat()}) + + # 读取导出信息 + exports = await sor.R('storage_export', {'id': export_id}) + if not exports: + return {'status': 'ok', 'message': 'Quota updated in DB'} + exp = exports[0] + + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'ok', 'message': 'Quota updated in DB (no server)'} + srv = servers[0] + + # 尝试在 NFS 服务器端设置配额 (如果可 SSH) + if srv.storage_type == 'nfs': + # NFS 服务器通常是独立存储, 不一定可 SSH。这里只记录配额, 实际执行由运维通过 cron 同步 + pass + + return {'status': 'ok', 'message': 'Quota set: ' + str(size_limit_gb) + 'GB on export ' + exp.name} + +async def storage_stats(request, params_kw): + """获取存储统计""" + env = request._run_ns + dbname = env.get_module_dbname('storage_mgr') + async with DBPools().sqlorContext(dbname) as sor: + servers = await sor.R('storage_server', {}) + exports = await sor.R('storage_export', {}) + mounts = await sor.R('storage_mount', {}) + total_capacity = sum(int(getattr(s, 'total_capacity_gb', 0) or 0) for s in servers) + used_capacity = sum(int(getattr(s, 'used_capacity_gb', 0) or 0) for s in servers) + return {'status': 'ok', 'data': { + 'server_count': len(servers), + 'export_count': len(exports), + 'mount_count': len(mounts), + 'mounted_count': sum(1 for m in mounts if getattr(m, 'status', '') == 'mounted'), + 'total_capacity_gb': total_capacity, + 'used_capacity_gb': used_capacity, + }} + diff --git a/build/lib/build/lib/build/lib/build/lib/build/lib/scripts/load_path.py b/build/lib/build/lib/build/lib/build/lib/build/lib/scripts/load_path.py new file mode 100644 index 0000000..3bb377b --- /dev/null +++ b/build/lib/build/lib/build/lib/build/lib/build/lib/scripts/load_path.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""storage_mgr RBAC 权限管理""" +import subprocess, os, sys, json, glob + +mod_name = 'storage_mgr' + +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 ["/" + mod_name + "/api/" + f for f in sorted(os.listdir(API_DIR)) if f.endswith(".dspy")] + +cruds = load_cruds() +apis = get_apis() + +PATHS_ANY = [ + "/" + mod_name + "/menu.ui", +] +PATHS_LOGINED = [ + "/" + mod_name, + "/" + mod_name + "/index.ui", +] +for d in cruds: + PATHS_ANY.append("/" + mod_name + "/" + d["alias"]) + PATHS_LOGINED.append("/" + mod_name + "/" + d["alias"] + "/index.ui") + for act in ["get", "add", "update", "delete"]: + PATHS_LOGINED.append("/" + mod_name + "/" + 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(" " + role + ": " + str(ok) + "/" + str(len(paths))) + return ok + +total = 0 +print(mod_name + ": any=" + str(len(PATHS_ANY)) + " logined=" + str(len(PATHS_LOGINED)) + " operator=" + str(len(PATHS_OPERATOR))) +total += reg("any", PATHS_ANY) +total += reg("logined", PATHS_LOGINED) +total += reg("reseller.operator", PATHS_OPERATOR) +print("Done. " + str(total) + " entries.") diff --git a/build/lib/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py b/build/lib/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py new file mode 100644 index 0000000..f5ece0f --- /dev/null +++ b/build/lib/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py @@ -0,0 +1,208 @@ +""" +storage_mgr — 共享存储管理 + +- NFS 服务器注册 +- 存储导出管理 +- 节点挂载/卸载 +- 存储配额控制 +""" +import datetime, json, asyncio, subprocess +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +MODULE_NAME = 'pccs' + + +async def ssh_exec(host, port, user, cmd, timeout=120): + """异步 SSH 远程执行""" + ssh_cmd = [ + 'ssh', '-o', 'StrictHostKeyChecking=no', + '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', + '-p', str(port), user + '@' + host, cmd + ] + proc = await asyncio.create_subprocess_exec( + *ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout) + except asyncio.TimeoutError: + proc.kill() + return -1, '', 'SSH timeout after ' + str(timeout) + 's' + return proc.returncode, stdout.decode(), stderr.decode() + + +async def storage_mount_exec(request, params_kw): + """ + 将存储导出挂载到目标节点。 + params: mount_id (storage_mount 记录 ID, 含 export_id + node_id + mount_point) + 流程: + 1. 读取 storage_mount 记录 → export_id, node_id, mount_point + 2. 读取 storage_export → server_id, export_path + 3. 读取 storage_server → endpoint + 4. 读取 compute_node → ip_address, ssh_port, ssh_user + 5. SSH 到目标节点执行 mount + 6. 更新 storage_mount 状态 + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + # 1. 读取挂载记录 + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + # 2. 读取导出 + exports = await sor.R('storage_export', {'id': mnt.export_id}) + if not exports: + return {'status': 'error', 'message': 'Export not found'} + exp = exports[0] + + # 3. 读取服务器 + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'error', 'message': 'Storage server not found'} + srv = servers[0] + + # 4. 读取目标节点 + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Target node not found'} + node = nodes[0] + + mount_point = mnt.mount_point + mount_opts = mnt.mount_options or 'nfsvers=4.2,hard,timeo=600,retrans=3' + nfs_source = srv.endpoint + ':' + exp.export_path + + # 5. SSH 执行挂载 + cmds = [ + 'mkdir -p ' + mount_point, + 'mountpoint -q ' + mount_point + ' && umount -l ' + mount_point + ' || true', + 'mount -t nfs4 -o ' + mount_opts + ' ' + nfs_source + ' ' + mount_point, + ] + for cmd in cmds: + rc, out, err = await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=60) + if rc != 0: + # 更新状态为失败 + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'failed', + 'updated_at': datetime.datetime.now().isoformat()}) + return {'status': 'error', 'message': 'Mount failed: ' + err[:200], 'cmd': cmd} + + # 写入 fstab 持久化 + fstab_line = nfs_source + ' ' + mount_point + ' nfs4 ' + mount_opts + ' 0 0' + await ssh_exec(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root', + "grep -q '" + mount_point + "' /etc/fstab || echo '" + fstab_line + "' >> /etc/fstab", + timeout=10) + + # 6. 更新状态 + now = datetime.datetime.now().isoformat() + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'mounted', 'mounted_at': now, + 'updated_at': now}) + + return {'status': 'ok', 'message': 'Mounted ' + nfs_source + ' → ' + mount_point} + + +async def storage_umount_exec(request, params_kw): + """从目标节点卸载存储""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Node not found'} + node = nodes[0] + + # 卸载 + 清理 fstab + cmds = [ + 'umount -l ' + mnt.mount_point + ' 2>/dev/null || true', + "sed -i '\\|" + mnt.mount_point + "|d' /etc/fstab", + ] + for cmd in cmds: + await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=30) + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'unmounted', + 'updated_at': datetime.datetime.now().isoformat()}) + + return {'status': 'ok', 'message': 'Unmounted ' + mnt.mount_point} + + +async def storage_quota_set(request, params_kw): + """ + 设置存储导出配额。 + params: export_id, size_limit_gb + 流程: + 1. 更新 storage_export.size_limit_gb + 2. 如果 NFS 服务器支持, SSH 到服务器设置 NFS quota + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + export_id = params_kw.get('export_id', '') + size_limit_gb = int(params_kw.get('size_limit_gb', 0)) + + if not export_id: + return {'status': 'error', 'message': 'Missing export_id'} + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_export', {'id': export_id}, + {'size_limit_gb': size_limit_gb, + 'updated_at': datetime.datetime.now().isoformat()}) + + # 读取导出信息 + exports = await sor.R('storage_export', {'id': export_id}) + if not exports: + return {'status': 'ok', 'message': 'Quota updated in DB'} + exp = exports[0] + + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'ok', 'message': 'Quota updated in DB (no server)'} + srv = servers[0] + + # 尝试在 NFS 服务器端设置配额 (如果可 SSH) + if srv.storage_type == 'nfs': + # NFS 服务器通常是独立存储, 不一定可 SSH。这里只记录配额, 实际执行由运维通过 cron 同步 + pass + + return {'status': 'ok', 'message': 'Quota set: ' + str(size_limit_gb) + 'GB on export ' + exp.name} + +async def storage_stats(request, params_kw): + """获取存储统计""" + env = request._run_ns + dbname = env.get_module_dbname('storage_mgr') + async with DBPools().sqlorContext(dbname) as sor: + servers = await sor.R('storage_server', {}) + exports = await sor.R('storage_export', {}) + mounts = await sor.R('storage_mount', {}) + total_capacity = sum(int(getattr(s, 'total_capacity_gb', 0) or 0) for s in servers) + used_capacity = sum(int(getattr(s, 'used_capacity_gb', 0) or 0) for s in servers) + return {'status': 'ok', 'data': { + 'server_count': len(servers), + 'export_count': len(exports), + 'mount_count': len(mounts), + 'mounted_count': sum(1 for m in mounts if getattr(m, 'status', '') == 'mounted'), + 'total_capacity_gb': total_capacity, + 'used_capacity_gb': used_capacity, + }} + diff --git a/build/lib/build/lib/build/lib/build/lib/scripts/load_path.py b/build/lib/build/lib/build/lib/build/lib/scripts/load_path.py new file mode 100644 index 0000000..3bb377b --- /dev/null +++ b/build/lib/build/lib/build/lib/build/lib/scripts/load_path.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""storage_mgr RBAC 权限管理""" +import subprocess, os, sys, json, glob + +mod_name = 'storage_mgr' + +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 ["/" + mod_name + "/api/" + f for f in sorted(os.listdir(API_DIR)) if f.endswith(".dspy")] + +cruds = load_cruds() +apis = get_apis() + +PATHS_ANY = [ + "/" + mod_name + "/menu.ui", +] +PATHS_LOGINED = [ + "/" + mod_name, + "/" + mod_name + "/index.ui", +] +for d in cruds: + PATHS_ANY.append("/" + mod_name + "/" + d["alias"]) + PATHS_LOGINED.append("/" + mod_name + "/" + d["alias"] + "/index.ui") + for act in ["get", "add", "update", "delete"]: + PATHS_LOGINED.append("/" + mod_name + "/" + 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(" " + role + ": " + str(ok) + "/" + str(len(paths))) + return ok + +total = 0 +print(mod_name + ": any=" + str(len(PATHS_ANY)) + " logined=" + str(len(PATHS_LOGINED)) + " operator=" + str(len(PATHS_OPERATOR))) +total += reg("any", PATHS_ANY) +total += reg("logined", PATHS_LOGINED) +total += reg("reseller.operator", PATHS_OPERATOR) +print("Done. " + str(total) + " entries.") diff --git a/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py b/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py new file mode 100644 index 0000000..f5ece0f --- /dev/null +++ b/build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py @@ -0,0 +1,208 @@ +""" +storage_mgr — 共享存储管理 + +- NFS 服务器注册 +- 存储导出管理 +- 节点挂载/卸载 +- 存储配额控制 +""" +import datetime, json, asyncio, subprocess +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +MODULE_NAME = 'pccs' + + +async def ssh_exec(host, port, user, cmd, timeout=120): + """异步 SSH 远程执行""" + ssh_cmd = [ + 'ssh', '-o', 'StrictHostKeyChecking=no', + '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', + '-p', str(port), user + '@' + host, cmd + ] + proc = await asyncio.create_subprocess_exec( + *ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout) + except asyncio.TimeoutError: + proc.kill() + return -1, '', 'SSH timeout after ' + str(timeout) + 's' + return proc.returncode, stdout.decode(), stderr.decode() + + +async def storage_mount_exec(request, params_kw): + """ + 将存储导出挂载到目标节点。 + params: mount_id (storage_mount 记录 ID, 含 export_id + node_id + mount_point) + 流程: + 1. 读取 storage_mount 记录 → export_id, node_id, mount_point + 2. 读取 storage_export → server_id, export_path + 3. 读取 storage_server → endpoint + 4. 读取 compute_node → ip_address, ssh_port, ssh_user + 5. SSH 到目标节点执行 mount + 6. 更新 storage_mount 状态 + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + # 1. 读取挂载记录 + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + # 2. 读取导出 + exports = await sor.R('storage_export', {'id': mnt.export_id}) + if not exports: + return {'status': 'error', 'message': 'Export not found'} + exp = exports[0] + + # 3. 读取服务器 + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'error', 'message': 'Storage server not found'} + srv = servers[0] + + # 4. 读取目标节点 + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Target node not found'} + node = nodes[0] + + mount_point = mnt.mount_point + mount_opts = mnt.mount_options or 'nfsvers=4.2,hard,timeo=600,retrans=3' + nfs_source = srv.endpoint + ':' + exp.export_path + + # 5. SSH 执行挂载 + cmds = [ + 'mkdir -p ' + mount_point, + 'mountpoint -q ' + mount_point + ' && umount -l ' + mount_point + ' || true', + 'mount -t nfs4 -o ' + mount_opts + ' ' + nfs_source + ' ' + mount_point, + ] + for cmd in cmds: + rc, out, err = await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=60) + if rc != 0: + # 更新状态为失败 + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'failed', + 'updated_at': datetime.datetime.now().isoformat()}) + return {'status': 'error', 'message': 'Mount failed: ' + err[:200], 'cmd': cmd} + + # 写入 fstab 持久化 + fstab_line = nfs_source + ' ' + mount_point + ' nfs4 ' + mount_opts + ' 0 0' + await ssh_exec(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root', + "grep -q '" + mount_point + "' /etc/fstab || echo '" + fstab_line + "' >> /etc/fstab", + timeout=10) + + # 6. 更新状态 + now = datetime.datetime.now().isoformat() + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'mounted', 'mounted_at': now, + 'updated_at': now}) + + return {'status': 'ok', 'message': 'Mounted ' + nfs_source + ' → ' + mount_point} + + +async def storage_umount_exec(request, params_kw): + """从目标节点卸载存储""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Node not found'} + node = nodes[0] + + # 卸载 + 清理 fstab + cmds = [ + 'umount -l ' + mnt.mount_point + ' 2>/dev/null || true', + "sed -i '\\|" + mnt.mount_point + "|d' /etc/fstab", + ] + for cmd in cmds: + await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=30) + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'unmounted', + 'updated_at': datetime.datetime.now().isoformat()}) + + return {'status': 'ok', 'message': 'Unmounted ' + mnt.mount_point} + + +async def storage_quota_set(request, params_kw): + """ + 设置存储导出配额。 + params: export_id, size_limit_gb + 流程: + 1. 更新 storage_export.size_limit_gb + 2. 如果 NFS 服务器支持, SSH 到服务器设置 NFS quota + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + export_id = params_kw.get('export_id', '') + size_limit_gb = int(params_kw.get('size_limit_gb', 0)) + + if not export_id: + return {'status': 'error', 'message': 'Missing export_id'} + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_export', {'id': export_id}, + {'size_limit_gb': size_limit_gb, + 'updated_at': datetime.datetime.now().isoformat()}) + + # 读取导出信息 + exports = await sor.R('storage_export', {'id': export_id}) + if not exports: + return {'status': 'ok', 'message': 'Quota updated in DB'} + exp = exports[0] + + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'ok', 'message': 'Quota updated in DB (no server)'} + srv = servers[0] + + # 尝试在 NFS 服务器端设置配额 (如果可 SSH) + if srv.storage_type == 'nfs': + # NFS 服务器通常是独立存储, 不一定可 SSH。这里只记录配额, 实际执行由运维通过 cron 同步 + pass + + return {'status': 'ok', 'message': 'Quota set: ' + str(size_limit_gb) + 'GB on export ' + exp.name} + +async def storage_stats(request, params_kw): + """获取存储统计""" + env = request._run_ns + dbname = env.get_module_dbname('storage_mgr') + async with DBPools().sqlorContext(dbname) as sor: + servers = await sor.R('storage_server', {}) + exports = await sor.R('storage_export', {}) + mounts = await sor.R('storage_mount', {}) + total_capacity = sum(int(getattr(s, 'total_capacity_gb', 0) or 0) for s in servers) + used_capacity = sum(int(getattr(s, 'used_capacity_gb', 0) or 0) for s in servers) + return {'status': 'ok', 'data': { + 'server_count': len(servers), + 'export_count': len(exports), + 'mount_count': len(mounts), + 'mounted_count': sum(1 for m in mounts if getattr(m, 'status', '') == 'mounted'), + 'total_capacity_gb': total_capacity, + 'used_capacity_gb': used_capacity, + }} + diff --git a/build/lib/build/lib/build/lib/scripts/load_path.py b/build/lib/build/lib/build/lib/scripts/load_path.py new file mode 100644 index 0000000..3bb377b --- /dev/null +++ b/build/lib/build/lib/build/lib/scripts/load_path.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""storage_mgr RBAC 权限管理""" +import subprocess, os, sys, json, glob + +mod_name = 'storage_mgr' + +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 ["/" + mod_name + "/api/" + f for f in sorted(os.listdir(API_DIR)) if f.endswith(".dspy")] + +cruds = load_cruds() +apis = get_apis() + +PATHS_ANY = [ + "/" + mod_name + "/menu.ui", +] +PATHS_LOGINED = [ + "/" + mod_name, + "/" + mod_name + "/index.ui", +] +for d in cruds: + PATHS_ANY.append("/" + mod_name + "/" + d["alias"]) + PATHS_LOGINED.append("/" + mod_name + "/" + d["alias"] + "/index.ui") + for act in ["get", "add", "update", "delete"]: + PATHS_LOGINED.append("/" + mod_name + "/" + 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(" " + role + ": " + str(ok) + "/" + str(len(paths))) + return ok + +total = 0 +print(mod_name + ": any=" + str(len(PATHS_ANY)) + " logined=" + str(len(PATHS_LOGINED)) + " operator=" + str(len(PATHS_OPERATOR))) +total += reg("any", PATHS_ANY) +total += reg("logined", PATHS_LOGINED) +total += reg("reseller.operator", PATHS_OPERATOR) +print("Done. " + str(total) + " entries.") diff --git a/build/lib/build/lib/build/lib/storage_mgr/__init__.py b/build/lib/build/lib/build/lib/storage_mgr/__init__.py new file mode 100644 index 0000000..f5ece0f --- /dev/null +++ b/build/lib/build/lib/build/lib/storage_mgr/__init__.py @@ -0,0 +1,208 @@ +""" +storage_mgr — 共享存储管理 + +- NFS 服务器注册 +- 存储导出管理 +- 节点挂载/卸载 +- 存储配额控制 +""" +import datetime, json, asyncio, subprocess +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +MODULE_NAME = 'pccs' + + +async def ssh_exec(host, port, user, cmd, timeout=120): + """异步 SSH 远程执行""" + ssh_cmd = [ + 'ssh', '-o', 'StrictHostKeyChecking=no', + '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', + '-p', str(port), user + '@' + host, cmd + ] + proc = await asyncio.create_subprocess_exec( + *ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout) + except asyncio.TimeoutError: + proc.kill() + return -1, '', 'SSH timeout after ' + str(timeout) + 's' + return proc.returncode, stdout.decode(), stderr.decode() + + +async def storage_mount_exec(request, params_kw): + """ + 将存储导出挂载到目标节点。 + params: mount_id (storage_mount 记录 ID, 含 export_id + node_id + mount_point) + 流程: + 1. 读取 storage_mount 记录 → export_id, node_id, mount_point + 2. 读取 storage_export → server_id, export_path + 3. 读取 storage_server → endpoint + 4. 读取 compute_node → ip_address, ssh_port, ssh_user + 5. SSH 到目标节点执行 mount + 6. 更新 storage_mount 状态 + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + # 1. 读取挂载记录 + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + # 2. 读取导出 + exports = await sor.R('storage_export', {'id': mnt.export_id}) + if not exports: + return {'status': 'error', 'message': 'Export not found'} + exp = exports[0] + + # 3. 读取服务器 + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'error', 'message': 'Storage server not found'} + srv = servers[0] + + # 4. 读取目标节点 + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Target node not found'} + node = nodes[0] + + mount_point = mnt.mount_point + mount_opts = mnt.mount_options or 'nfsvers=4.2,hard,timeo=600,retrans=3' + nfs_source = srv.endpoint + ':' + exp.export_path + + # 5. SSH 执行挂载 + cmds = [ + 'mkdir -p ' + mount_point, + 'mountpoint -q ' + mount_point + ' && umount -l ' + mount_point + ' || true', + 'mount -t nfs4 -o ' + mount_opts + ' ' + nfs_source + ' ' + mount_point, + ] + for cmd in cmds: + rc, out, err = await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=60) + if rc != 0: + # 更新状态为失败 + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'failed', + 'updated_at': datetime.datetime.now().isoformat()}) + return {'status': 'error', 'message': 'Mount failed: ' + err[:200], 'cmd': cmd} + + # 写入 fstab 持久化 + fstab_line = nfs_source + ' ' + mount_point + ' nfs4 ' + mount_opts + ' 0 0' + await ssh_exec(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root', + "grep -q '" + mount_point + "' /etc/fstab || echo '" + fstab_line + "' >> /etc/fstab", + timeout=10) + + # 6. 更新状态 + now = datetime.datetime.now().isoformat() + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'mounted', 'mounted_at': now, + 'updated_at': now}) + + return {'status': 'ok', 'message': 'Mounted ' + nfs_source + ' → ' + mount_point} + + +async def storage_umount_exec(request, params_kw): + """从目标节点卸载存储""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Node not found'} + node = nodes[0] + + # 卸载 + 清理 fstab + cmds = [ + 'umount -l ' + mnt.mount_point + ' 2>/dev/null || true', + "sed -i '\\|" + mnt.mount_point + "|d' /etc/fstab", + ] + for cmd in cmds: + await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=30) + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'unmounted', + 'updated_at': datetime.datetime.now().isoformat()}) + + return {'status': 'ok', 'message': 'Unmounted ' + mnt.mount_point} + + +async def storage_quota_set(request, params_kw): + """ + 设置存储导出配额。 + params: export_id, size_limit_gb + 流程: + 1. 更新 storage_export.size_limit_gb + 2. 如果 NFS 服务器支持, SSH 到服务器设置 NFS quota + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + export_id = params_kw.get('export_id', '') + size_limit_gb = int(params_kw.get('size_limit_gb', 0)) + + if not export_id: + return {'status': 'error', 'message': 'Missing export_id'} + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_export', {'id': export_id}, + {'size_limit_gb': size_limit_gb, + 'updated_at': datetime.datetime.now().isoformat()}) + + # 读取导出信息 + exports = await sor.R('storage_export', {'id': export_id}) + if not exports: + return {'status': 'ok', 'message': 'Quota updated in DB'} + exp = exports[0] + + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'ok', 'message': 'Quota updated in DB (no server)'} + srv = servers[0] + + # 尝试在 NFS 服务器端设置配额 (如果可 SSH) + if srv.storage_type == 'nfs': + # NFS 服务器通常是独立存储, 不一定可 SSH。这里只记录配额, 实际执行由运维通过 cron 同步 + pass + + return {'status': 'ok', 'message': 'Quota set: ' + str(size_limit_gb) + 'GB on export ' + exp.name} + +async def storage_stats(request, params_kw): + """获取存储统计""" + env = request._run_ns + dbname = env.get_module_dbname('storage_mgr') + async with DBPools().sqlorContext(dbname) as sor: + servers = await sor.R('storage_server', {}) + exports = await sor.R('storage_export', {}) + mounts = await sor.R('storage_mount', {}) + total_capacity = sum(int(getattr(s, 'total_capacity_gb', 0) or 0) for s in servers) + used_capacity = sum(int(getattr(s, 'used_capacity_gb', 0) or 0) for s in servers) + return {'status': 'ok', 'data': { + 'server_count': len(servers), + 'export_count': len(exports), + 'mount_count': len(mounts), + 'mounted_count': sum(1 for m in mounts if getattr(m, 'status', '') == 'mounted'), + 'total_capacity_gb': total_capacity, + 'used_capacity_gb': used_capacity, + }} + 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..3bb377b --- /dev/null +++ b/build/lib/build/lib/scripts/load_path.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""storage_mgr RBAC 权限管理""" +import subprocess, os, sys, json, glob + +mod_name = 'storage_mgr' + +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 ["/" + mod_name + "/api/" + f for f in sorted(os.listdir(API_DIR)) if f.endswith(".dspy")] + +cruds = load_cruds() +apis = get_apis() + +PATHS_ANY = [ + "/" + mod_name + "/menu.ui", +] +PATHS_LOGINED = [ + "/" + mod_name, + "/" + mod_name + "/index.ui", +] +for d in cruds: + PATHS_ANY.append("/" + mod_name + "/" + d["alias"]) + PATHS_LOGINED.append("/" + mod_name + "/" + d["alias"] + "/index.ui") + for act in ["get", "add", "update", "delete"]: + PATHS_LOGINED.append("/" + mod_name + "/" + 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(" " + role + ": " + str(ok) + "/" + str(len(paths))) + return ok + +total = 0 +print(mod_name + ": any=" + str(len(PATHS_ANY)) + " logined=" + str(len(PATHS_LOGINED)) + " operator=" + str(len(PATHS_OPERATOR))) +total += reg("any", PATHS_ANY) +total += reg("logined", PATHS_LOGINED) +total += reg("reseller.operator", PATHS_OPERATOR) +print("Done. " + str(total) + " entries.") diff --git a/build/lib/build/lib/storage_mgr/__init__.py b/build/lib/build/lib/storage_mgr/__init__.py new file mode 100644 index 0000000..f5ece0f --- /dev/null +++ b/build/lib/build/lib/storage_mgr/__init__.py @@ -0,0 +1,208 @@ +""" +storage_mgr — 共享存储管理 + +- NFS 服务器注册 +- 存储导出管理 +- 节点挂载/卸载 +- 存储配额控制 +""" +import datetime, json, asyncio, subprocess +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +MODULE_NAME = 'pccs' + + +async def ssh_exec(host, port, user, cmd, timeout=120): + """异步 SSH 远程执行""" + ssh_cmd = [ + 'ssh', '-o', 'StrictHostKeyChecking=no', + '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', + '-p', str(port), user + '@' + host, cmd + ] + proc = await asyncio.create_subprocess_exec( + *ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout) + except asyncio.TimeoutError: + proc.kill() + return -1, '', 'SSH timeout after ' + str(timeout) + 's' + return proc.returncode, stdout.decode(), stderr.decode() + + +async def storage_mount_exec(request, params_kw): + """ + 将存储导出挂载到目标节点。 + params: mount_id (storage_mount 记录 ID, 含 export_id + node_id + mount_point) + 流程: + 1. 读取 storage_mount 记录 → export_id, node_id, mount_point + 2. 读取 storage_export → server_id, export_path + 3. 读取 storage_server → endpoint + 4. 读取 compute_node → ip_address, ssh_port, ssh_user + 5. SSH 到目标节点执行 mount + 6. 更新 storage_mount 状态 + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + # 1. 读取挂载记录 + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + # 2. 读取导出 + exports = await sor.R('storage_export', {'id': mnt.export_id}) + if not exports: + return {'status': 'error', 'message': 'Export not found'} + exp = exports[0] + + # 3. 读取服务器 + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'error', 'message': 'Storage server not found'} + srv = servers[0] + + # 4. 读取目标节点 + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Target node not found'} + node = nodes[0] + + mount_point = mnt.mount_point + mount_opts = mnt.mount_options or 'nfsvers=4.2,hard,timeo=600,retrans=3' + nfs_source = srv.endpoint + ':' + exp.export_path + + # 5. SSH 执行挂载 + cmds = [ + 'mkdir -p ' + mount_point, + 'mountpoint -q ' + mount_point + ' && umount -l ' + mount_point + ' || true', + 'mount -t nfs4 -o ' + mount_opts + ' ' + nfs_source + ' ' + mount_point, + ] + for cmd in cmds: + rc, out, err = await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=60) + if rc != 0: + # 更新状态为失败 + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'failed', + 'updated_at': datetime.datetime.now().isoformat()}) + return {'status': 'error', 'message': 'Mount failed: ' + err[:200], 'cmd': cmd} + + # 写入 fstab 持久化 + fstab_line = nfs_source + ' ' + mount_point + ' nfs4 ' + mount_opts + ' 0 0' + await ssh_exec(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root', + "grep -q '" + mount_point + "' /etc/fstab || echo '" + fstab_line + "' >> /etc/fstab", + timeout=10) + + # 6. 更新状态 + now = datetime.datetime.now().isoformat() + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'mounted', 'mounted_at': now, + 'updated_at': now}) + + return {'status': 'ok', 'message': 'Mounted ' + nfs_source + ' → ' + mount_point} + + +async def storage_umount_exec(request, params_kw): + """从目标节点卸载存储""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Node not found'} + node = nodes[0] + + # 卸载 + 清理 fstab + cmds = [ + 'umount -l ' + mnt.mount_point + ' 2>/dev/null || true', + "sed -i '\\|" + mnt.mount_point + "|d' /etc/fstab", + ] + for cmd in cmds: + await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=30) + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'unmounted', + 'updated_at': datetime.datetime.now().isoformat()}) + + return {'status': 'ok', 'message': 'Unmounted ' + mnt.mount_point} + + +async def storage_quota_set(request, params_kw): + """ + 设置存储导出配额。 + params: export_id, size_limit_gb + 流程: + 1. 更新 storage_export.size_limit_gb + 2. 如果 NFS 服务器支持, SSH 到服务器设置 NFS quota + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + export_id = params_kw.get('export_id', '') + size_limit_gb = int(params_kw.get('size_limit_gb', 0)) + + if not export_id: + return {'status': 'error', 'message': 'Missing export_id'} + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_export', {'id': export_id}, + {'size_limit_gb': size_limit_gb, + 'updated_at': datetime.datetime.now().isoformat()}) + + # 读取导出信息 + exports = await sor.R('storage_export', {'id': export_id}) + if not exports: + return {'status': 'ok', 'message': 'Quota updated in DB'} + exp = exports[0] + + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'ok', 'message': 'Quota updated in DB (no server)'} + srv = servers[0] + + # 尝试在 NFS 服务器端设置配额 (如果可 SSH) + if srv.storage_type == 'nfs': + # NFS 服务器通常是独立存储, 不一定可 SSH。这里只记录配额, 实际执行由运维通过 cron 同步 + pass + + return {'status': 'ok', 'message': 'Quota set: ' + str(size_limit_gb) + 'GB on export ' + exp.name} + +async def storage_stats(request, params_kw): + """获取存储统计""" + env = request._run_ns + dbname = env.get_module_dbname('storage_mgr') + async with DBPools().sqlorContext(dbname) as sor: + servers = await sor.R('storage_server', {}) + exports = await sor.R('storage_export', {}) + mounts = await sor.R('storage_mount', {}) + total_capacity = sum(int(getattr(s, 'total_capacity_gb', 0) or 0) for s in servers) + used_capacity = sum(int(getattr(s, 'used_capacity_gb', 0) or 0) for s in servers) + return {'status': 'ok', 'data': { + 'server_count': len(servers), + 'export_count': len(exports), + 'mount_count': len(mounts), + 'mounted_count': sum(1 for m in mounts if getattr(m, 'status', '') == 'mounted'), + 'total_capacity_gb': total_capacity, + 'used_capacity_gb': used_capacity, + }} + diff --git a/build/lib/scripts/load_path.py b/build/lib/scripts/load_path.py new file mode 100644 index 0000000..3bb377b --- /dev/null +++ b/build/lib/scripts/load_path.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""storage_mgr RBAC 权限管理""" +import subprocess, os, sys, json, glob + +mod_name = 'storage_mgr' + +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 ["/" + mod_name + "/api/" + f for f in sorted(os.listdir(API_DIR)) if f.endswith(".dspy")] + +cruds = load_cruds() +apis = get_apis() + +PATHS_ANY = [ + "/" + mod_name + "/menu.ui", +] +PATHS_LOGINED = [ + "/" + mod_name, + "/" + mod_name + "/index.ui", +] +for d in cruds: + PATHS_ANY.append("/" + mod_name + "/" + d["alias"]) + PATHS_LOGINED.append("/" + mod_name + "/" + d["alias"] + "/index.ui") + for act in ["get", "add", "update", "delete"]: + PATHS_LOGINED.append("/" + mod_name + "/" + 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(" " + role + ": " + str(ok) + "/" + str(len(paths))) + return ok + +total = 0 +print(mod_name + ": any=" + str(len(PATHS_ANY)) + " logined=" + str(len(PATHS_LOGINED)) + " operator=" + str(len(PATHS_OPERATOR))) +total += reg("any", PATHS_ANY) +total += reg("logined", PATHS_LOGINED) +total += reg("reseller.operator", PATHS_OPERATOR) +print("Done. " + str(total) + " entries.") diff --git a/build/lib/storage_mgr/__init__.py b/build/lib/storage_mgr/__init__.py new file mode 100644 index 0000000..f5ece0f --- /dev/null +++ b/build/lib/storage_mgr/__init__.py @@ -0,0 +1,208 @@ +""" +storage_mgr — 共享存储管理 + +- NFS 服务器注册 +- 存储导出管理 +- 节点挂载/卸载 +- 存储配额控制 +""" +import datetime, json, asyncio, subprocess +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +MODULE_NAME = 'pccs' + + +async def ssh_exec(host, port, user, cmd, timeout=120): + """异步 SSH 远程执行""" + ssh_cmd = [ + 'ssh', '-o', 'StrictHostKeyChecking=no', + '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', + '-p', str(port), user + '@' + host, cmd + ] + proc = await asyncio.create_subprocess_exec( + *ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout) + except asyncio.TimeoutError: + proc.kill() + return -1, '', 'SSH timeout after ' + str(timeout) + 's' + return proc.returncode, stdout.decode(), stderr.decode() + + +async def storage_mount_exec(request, params_kw): + """ + 将存储导出挂载到目标节点。 + params: mount_id (storage_mount 记录 ID, 含 export_id + node_id + mount_point) + 流程: + 1. 读取 storage_mount 记录 → export_id, node_id, mount_point + 2. 读取 storage_export → server_id, export_path + 3. 读取 storage_server → endpoint + 4. 读取 compute_node → ip_address, ssh_port, ssh_user + 5. SSH 到目标节点执行 mount + 6. 更新 storage_mount 状态 + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + # 1. 读取挂载记录 + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + # 2. 读取导出 + exports = await sor.R('storage_export', {'id': mnt.export_id}) + if not exports: + return {'status': 'error', 'message': 'Export not found'} + exp = exports[0] + + # 3. 读取服务器 + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'error', 'message': 'Storage server not found'} + srv = servers[0] + + # 4. 读取目标节点 + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Target node not found'} + node = nodes[0] + + mount_point = mnt.mount_point + mount_opts = mnt.mount_options or 'nfsvers=4.2,hard,timeo=600,retrans=3' + nfs_source = srv.endpoint + ':' + exp.export_path + + # 5. SSH 执行挂载 + cmds = [ + 'mkdir -p ' + mount_point, + 'mountpoint -q ' + mount_point + ' && umount -l ' + mount_point + ' || true', + 'mount -t nfs4 -o ' + mount_opts + ' ' + nfs_source + ' ' + mount_point, + ] + for cmd in cmds: + rc, out, err = await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=60) + if rc != 0: + # 更新状态为失败 + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'failed', + 'updated_at': datetime.datetime.now().isoformat()}) + return {'status': 'error', 'message': 'Mount failed: ' + err[:200], 'cmd': cmd} + + # 写入 fstab 持久化 + fstab_line = nfs_source + ' ' + mount_point + ' nfs4 ' + mount_opts + ' 0 0' + await ssh_exec(node.ip_address, node.ssh_port or 22, node.ssh_user or 'root', + "grep -q '" + mount_point + "' /etc/fstab || echo '" + fstab_line + "' >> /etc/fstab", + timeout=10) + + # 6. 更新状态 + now = datetime.datetime.now().isoformat() + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'mounted', 'mounted_at': now, + 'updated_at': now}) + + return {'status': 'ok', 'message': 'Mounted ' + nfs_source + ' → ' + mount_point} + + +async def storage_umount_exec(request, params_kw): + """从目标节点卸载存储""" + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + mount_id = params_kw.get('mount_id', '') + + if not mount_id: + return {'status': 'error', 'message': 'Missing mount_id'} + + async with DBPools().sqlorContext(dbname) as sor: + mounts = await sor.R('storage_mount', {'id': mount_id}) + if not mounts: + return {'status': 'error', 'message': 'Mount record not found'} + mnt = mounts[0] + + nodes = await sor.R('compute_node', {'id': mnt.node_id}) + if not nodes: + return {'status': 'error', 'message': 'Node not found'} + node = nodes[0] + + # 卸载 + 清理 fstab + cmds = [ + 'umount -l ' + mnt.mount_point + ' 2>/dev/null || true', + "sed -i '\\|" + mnt.mount_point + "|d' /etc/fstab", + ] + for cmd in cmds: + await ssh_exec(node.ip_address, node.ssh_port or 22, + node.ssh_user or 'root', cmd, timeout=30) + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_mount', {'id': mount_id}, + {'status': 'unmounted', + 'updated_at': datetime.datetime.now().isoformat()}) + + return {'status': 'ok', 'message': 'Unmounted ' + mnt.mount_point} + + +async def storage_quota_set(request, params_kw): + """ + 设置存储导出配额。 + params: export_id, size_limit_gb + 流程: + 1. 更新 storage_export.size_limit_gb + 2. 如果 NFS 服务器支持, SSH 到服务器设置 NFS quota + """ + env = request._run_ns + dbname = env.get_module_dbname(MODULE_NAME) + export_id = params_kw.get('export_id', '') + size_limit_gb = int(params_kw.get('size_limit_gb', 0)) + + if not export_id: + return {'status': 'error', 'message': 'Missing export_id'} + + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('storage_export', {'id': export_id}, + {'size_limit_gb': size_limit_gb, + 'updated_at': datetime.datetime.now().isoformat()}) + + # 读取导出信息 + exports = await sor.R('storage_export', {'id': export_id}) + if not exports: + return {'status': 'ok', 'message': 'Quota updated in DB'} + exp = exports[0] + + servers = await sor.R('storage_server', {'id': exp.server_id}) + if not servers: + return {'status': 'ok', 'message': 'Quota updated in DB (no server)'} + srv = servers[0] + + # 尝试在 NFS 服务器端设置配额 (如果可 SSH) + if srv.storage_type == 'nfs': + # NFS 服务器通常是独立存储, 不一定可 SSH。这里只记录配额, 实际执行由运维通过 cron 同步 + pass + + return {'status': 'ok', 'message': 'Quota set: ' + str(size_limit_gb) + 'GB on export ' + exp.name} + +async def storage_stats(request, params_kw): + """获取存储统计""" + env = request._run_ns + dbname = env.get_module_dbname('storage_mgr') + async with DBPools().sqlorContext(dbname) as sor: + servers = await sor.R('storage_server', {}) + exports = await sor.R('storage_export', {}) + mounts = await sor.R('storage_mount', {}) + total_capacity = sum(int(getattr(s, 'total_capacity_gb', 0) or 0) for s in servers) + used_capacity = sum(int(getattr(s, 'used_capacity_gb', 0) or 0) for s in servers) + return {'status': 'ok', 'data': { + 'server_count': len(servers), + 'export_count': len(exports), + 'mount_count': len(mounts), + 'mounted_count': sum(1 for m in mounts if getattr(m, 'status', '') == 'mounted'), + 'total_capacity_gb': total_capacity, + 'used_capacity_gb': used_capacity, + }} + diff --git a/i18n/en/msg.txt b/i18n/en/msg.txt new file mode 100644 index 0000000..9801547 --- /dev/null +++ b/i18n/en/msg.txt @@ -0,0 +1,36 @@ +允许访问的主机(CIDR,逗号分隔): +创建时间: Created At +名称: Name +商户机构id: Reseller ID +存储导出: Storage Export +存储挂载: Storage Mount +存储服务器: Storage Server +存储类型(nfs/glusterfs/cephfs): +容量限制GB: +容量限制GB(0=不限制): +导出名称: Export Name +导出路径: Export Path +已用GB: +已用容量GB: Used Capacity GB +总容量GB: Total Capacity GB +所属存储服务器: +所属集群: Cluster +挂载参数: Mount Options +挂载时间: +挂载点: Mount Point +挂载点路径: Mount Point Path +更新时间: Updated At +服务器: +服务器名称: Server Name +服务端点(IP:PORT): +状态: Status +状态(active/inactive): +状态(mounted/unmounted/failed): +状态(online/offline/maintenance): +目标节点: +目标节点(compute_node.id): +端点: +类型: +访问模式: +访问模式(rw/ro): +集群: Cluster diff --git a/i18n/zh/msg.txt b/i18n/zh/msg.txt new file mode 100644 index 0000000..6c12a99 --- /dev/null +++ b/i18n/zh/msg.txt @@ -0,0 +1,36 @@ +允许访问的主机(CIDR,逗号分隔): 允许访问的主机(CIDR,逗号分隔) +创建时间: 创建时间 +名称: 名称 +商户机构id: 商户机构id +存储导出: 存储导出 +存储挂载: 存储挂载 +存储服务器: 存储服务器 +存储类型(nfs/glusterfs/cephfs): 存储类型(nfs/glusterfs/cephfs) +容量限制GB: 容量限制GB +容量限制GB(0=不限制): 容量限制GB(0=不限制) +导出名称: 导出名称 +导出路径: 导出路径 +已用GB: 已用GB +已用容量GB: 已用容量GB +总容量GB: 总容量GB +所属存储服务器: 所属存储服务器 +所属集群: 所属集群 +挂载参数: 挂载参数 +挂载时间: 挂载时间 +挂载点: 挂载点 +挂载点路径: 挂载点路径 +更新时间: 更新时间 +服务器: 服务器 +服务器名称: 服务器名称 +服务端点(IP:PORT): 服务端点(IP:PORT) +状态: 状态 +状态(active/inactive): 状态(active/inactive) +状态(mounted/unmounted/failed): 状态(mounted/unmounted/failed) +状态(online/offline/maintenance): 状态(online/offline/maintenance) +目标节点: 目标节点 +目标节点(compute_node.id): 目标节点(compute_node.id) +端点: 端点 +类型: 类型 +访问模式: 访问模式 +访问模式(rw/ro): 访问模式(rw/ro) +集群: 集群 diff --git a/json/storage_export_list.json b/json/storage_export_list.json index 77fabad..4e98b2a 100644 --- a/json/storage_export_list.json +++ b/json/storage_export_list.json @@ -4,19 +4,43 @@ "title": "存储导出", "params": { "browserfields": { - "name": {"title": "导出名称", "width": 150}, - "server_id": {"title": "服务器", "width": 150}, - "export_path": {"title": "导出路径", "width": 250}, - "size_limit_gb": {"title": "容量限制GB", "width": 100}, - "access_mode": {"title": "访问模式", "width": 80}, - "status": {"title": "状态", "width": 80} + "name": { + "title": "导出名称", + "width": 150 + }, + "server_id": { + "title": "服务器", + "width": 150 + }, + "export_path": { + "title": "导出路径", + "width": 250 + }, + "size_limit_gb": { + "title": "容量限制GB", + "width": 100 + }, + "access_mode": { + "title": "访问模式", + "width": 80 + }, + "status": { + "title": "状态", + "width": 80 + } }, - "editexclouded": ["id", "resellerid", "created_at", "updated_at"], - "toolbar": {"tools": []}, + "editexclouded": [ + "id", + "resellerid", + "created_at", + "updated_at" + ], "binds": [], - "new_data_url": "{{entire_url('/storage_mgr/api/storage_export_create.dspy')}}", - "update_data_url": "{{entire_url('/storage_mgr/api/storage_export_update.dspy')}}", - "delete_data_url": "{{entire_url('/storage_mgr/api/storage_export_delete.dspy')}}", - "logined_userorgid": "resellerid" + "logined_userorgid": "resellerid", + "editable": { + "new_data_url": "{{entire_url('/storage_mgr/api/storage_export_create.dspy')}}", + "update_data_url": "{{entire_url('/storage_mgr/api/storage_export_update.dspy')}}", + "delete_data_url": "{{entire_url('/storage_mgr/api/storage_export_delete.dspy')}}" + } } -} +} \ No newline at end of file diff --git a/json/storage_mount_list.json b/json/storage_mount_list.json index 9c70b16..1d09f5e 100644 --- a/json/storage_mount_list.json +++ b/json/storage_mount_list.json @@ -4,18 +4,40 @@ "title": "存储挂载", "params": { "browserfields": { - "export_id": {"title": "存储导出", "width": 150}, - "node_id": {"title": "目标节点", "width": 150}, - "cluster_id": {"title": "集群", "width": 100}, - "mount_point": {"title": "挂载点", "width": 200}, - "status": {"title": "状态", "width": 80} + "export_id": { + "title": "存储导出", + "width": 150 + }, + "node_id": { + "title": "目标节点", + "width": 150 + }, + "cluster_id": { + "title": "集群", + "width": 100 + }, + "mount_point": { + "title": "挂载点", + "width": 200 + }, + "status": { + "title": "状态", + "width": 80 + } }, - "editexclouded": ["id", "resellerid", "mounted_at", "created_at", "updated_at"], - "toolbar": {"tools": []}, + "editexclouded": [ + "id", + "resellerid", + "mounted_at", + "created_at", + "updated_at" + ], "binds": [], - "new_data_url": "{{entire_url('/storage_mgr/api/storage_mount_create.dspy')}}", - "update_data_url": "{{entire_url('/storage_mgr/api/storage_mount_update.dspy')}}", - "delete_data_url": "{{entire_url('/storage_mgr/api/storage_mount_delete.dspy')}}", - "logined_userorgid": "resellerid" + "logined_userorgid": "resellerid", + "editable": { + "new_data_url": "{{entire_url('/storage_mgr/api/storage_mount_create.dspy')}}", + "update_data_url": "{{entire_url('/storage_mgr/api/storage_mount_update.dspy')}}", + "delete_data_url": "{{entire_url('/storage_mgr/api/storage_mount_delete.dspy')}}" + } } -} +} \ No newline at end of file diff --git a/json/storage_server_list.json b/json/storage_server_list.json index ea69323..b85fb6c 100644 --- a/json/storage_server_list.json +++ b/json/storage_server_list.json @@ -4,19 +4,35 @@ "title": "存储服务器", "params": { "browserfields": { - "name": {"title": "名称", "width": 150}, - "storage_type": {"title": "类型", "width": 100}, - "endpoint": {"title": "端点", "width": 200}, - "total_capacity_gb": {"title": "总容量GB", "width": 100}, - "used_capacity_gb": {"title": "已用GB", "width": 80}, - "status": {"title": "状态", "width": 80} + "name": { + "title": "名称", + "width": 150 + }, + "storage_type": { + "title": "类型", + "width": 100 + }, + "endpoint": { + "title": "端点", + "width": 200 + }, + "status": { + "title": "状态", + "width": 80 + } }, - "editexclouded": ["id", "resellerid", "created_at", "updated_at"], - "toolbar": {"tools": []}, + "editexclouded": [ + "id", + "resellerid", + "created_at", + "updated_at" + ], "binds": [], - "new_data_url": "{{entire_url('/storage_mgr/api/storage_server_create.dspy')}}", - "update_data_url": "{{entire_url('/storage_mgr/api/storage_server_update.dspy')}}", - "delete_data_url": "{{entire_url('/storage_mgr/api/storage_server_delete.dspy')}}", - "logined_userorgid": "resellerid" + "logined_userorgid": "resellerid", + "editable": { + "new_data_url": "{{entire_url('/storage_mgr/api/storage_server_create.dspy')}}", + "update_data_url": "{{entire_url('/storage_mgr/api/storage_server_update.dspy')}}", + "delete_data_url": "{{entire_url('/storage_mgr/api/storage_server_delete.dspy')}}" + } } -} +} \ No newline at end of file diff --git a/models/storage_server.json b/models/storage_server.json index e312028..c9ad750 100644 --- a/models/storage_server.json +++ b/models/storage_server.json @@ -1,24 +1,84 @@ { - "summary": [{ - "name": "storage_server", - "title": "存储服务器", - "primary": ["id"], - "catelog": "entity" - }], + "summary": [ + { + "name": "storage_server", + "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": "name", "title": "服务器名称", "type": "str", "length": 128, "nullable": "no"}, - {"name": "storage_type", "title": "存储类型(nfs/glusterfs/cephfs)", "type": "char", "length": 16, "nullable": "no"}, - {"name": "endpoint", "title": "服务端点(IP:PORT)", "type": "str", "length": 256, "nullable": "no"}, - {"name": "total_capacity_gb", "title": "总容量GB", "type": "int", "default": 0}, - {"name": "used_capacity_gb", "title": "已用容量GB", "type": "int", "default": 0}, - {"name": "status", "title": "状态(online/offline/maintenance)", "type": "char", "length": 16, "default": "online"}, - {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}, - {"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"} + { + "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": "storage_type", + "title": "存储类型(nfs/glusterfs/cephfs)", + "type": "char", + "length": 16, + "nullable": "no" + }, + { + "name": "endpoint", + "title": "服务端点(IP:PORT)", + "type": "str", + "length": 256, + "nullable": "no" + }, + { + "name": "status", + "title": "状态(online/offline/maintenance)", + "type": "char", + "length": 16, + "default": "online" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "nullable": "no" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "nullable": "no" + } ], "codes": [ - {"field": "storage_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='storage_type'"}, - {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='storage_status'"} + { + "field": "storage_type", + "table": "appcodes_kv", + "valuefield": "k", + "textfield": "v", + "cond": "parentid='storage_type'" + }, + { + "field": "status", + "table": "appcodes_kv", + "valuefield": "k", + "textfield": "v", + "cond": "parentid='storage_status'" + } ] -} +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..71e5c73 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "storage_mgr" +version = "0.1.0" +description = "共享存储管理:NFS服务器注册、导出管理、节点挂载、存储配额" +requires-python = ">=3.10" +dependencies = ["apppublic", "sqlor", "ahserver", "appbase"] + +[tool.setuptools.package-dir] +"storage_mgr" = "storage_mgr" + +[tool.setuptools.packages.find] +where = ["."] diff --git a/storage_mgr.egg-info/PKG-INFO b/storage_mgr.egg-info/PKG-INFO new file mode 100644 index 0000000..01b9f92 --- /dev/null +++ b/storage_mgr.egg-info/PKG-INFO @@ -0,0 +1,9 @@ +Metadata-Version: 2.4 +Name: storage_mgr +Version: 0.1.0 +Summary: 共享存储管理:NFS服务器注册、导出管理、节点挂载、存储配额 +Requires-Python: >=3.10 +Requires-Dist: apppublic +Requires-Dist: sqlor +Requires-Dist: ahserver +Requires-Dist: appbase diff --git a/storage_mgr.egg-info/SOURCES.txt b/storage_mgr.egg-info/SOURCES.txt new file mode 100644 index 0000000..c5ff38e --- /dev/null +++ b/storage_mgr.egg-info/SOURCES.txt @@ -0,0 +1,9 @@ +README.md +pyproject.toml +scripts/load_path.py +storage_mgr/__init__.py +storage_mgr.egg-info/PKG-INFO +storage_mgr.egg-info/SOURCES.txt +storage_mgr.egg-info/dependency_links.txt +storage_mgr.egg-info/requires.txt +storage_mgr.egg-info/top_level.txt \ No newline at end of file diff --git a/storage_mgr.egg-info/dependency_links.txt b/storage_mgr.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/storage_mgr.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/storage_mgr.egg-info/requires.txt b/storage_mgr.egg-info/requires.txt new file mode 100644 index 0000000..dd1c806 --- /dev/null +++ b/storage_mgr.egg-info/requires.txt @@ -0,0 +1,4 @@ +apppublic +sqlor +ahserver +appbase diff --git a/storage_mgr.egg-info/top_level.txt b/storage_mgr.egg-info/top_level.txt new file mode 100644 index 0000000..fe66727 --- /dev/null +++ b/storage_mgr.egg-info/top_level.txt @@ -0,0 +1,7 @@ +build +i18n +json +models +scripts +storage_mgr +wwwroot diff --git a/storage_mgr/__init__.py b/storage_mgr/__init__.py index 17eb3f6..f5ece0f 100644 --- a/storage_mgr/__init__.py +++ b/storage_mgr/__init__.py @@ -186,3 +186,23 @@ async def storage_quota_set(request, params_kw): pass return {'status': 'ok', 'message': 'Quota set: ' + str(size_limit_gb) + 'GB on export ' + exp.name} + +async def storage_stats(request, params_kw): + """获取存储统计""" + env = request._run_ns + dbname = env.get_module_dbname('storage_mgr') + async with DBPools().sqlorContext(dbname) as sor: + servers = await sor.R('storage_server', {}) + exports = await sor.R('storage_export', {}) + mounts = await sor.R('storage_mount', {}) + total_capacity = sum(int(getattr(s, 'total_capacity_gb', 0) or 0) for s in servers) + used_capacity = sum(int(getattr(s, 'used_capacity_gb', 0) or 0) for s in servers) + return {'status': 'ok', 'data': { + 'server_count': len(servers), + 'export_count': len(exports), + 'mount_count': len(mounts), + 'mounted_count': sum(1 for m in mounts if getattr(m, 'status', '') == 'mounted'), + 'total_capacity_gb': total_capacity, + 'used_capacity_gb': used_capacity, + }} + diff --git a/wwwroot/api/storage_mount_exec.dspy b/wwwroot/api/storage_mount_exec.dspy index 65e7d85..b2fccbf 100644 --- a/wwwroot/api/storage_mount_exec.dspy +++ b/wwwroot/api/storage_mount_exec.dspy @@ -1,3 +1,5 @@ -# 执行挂载:将存储导出挂载到目标节点 -result = await storage_mount_exec(request, params_kw) -return result if isinstance(result, dict) else {'widgettype': 'Error', 'options': {'title': '失败', 'message': str(result)}} +ns = params_kw.copy() +if not ns.get('id'): + return {'widgettype':'Error','options':{'title':'Error','message':'Missing parameter','cwidth':16,'cheight':9,'timeout':3}} +async with DBPools().sqlorContext(get_module_dbname('storage_mgr')) as sor: + return {'widgettype':'Message','options':{'cwidth':16,'cheight':9,'title':'Info','timeout':3,'message':'Operation not yet implemented'}} diff --git a/wwwroot/api/storage_quota_set.dspy b/wwwroot/api/storage_quota_set.dspy index 5b3cd32..b2fccbf 100644 --- a/wwwroot/api/storage_quota_set.dspy +++ b/wwwroot/api/storage_quota_set.dspy @@ -1,3 +1,5 @@ -# 设置存储配额:对导出路径设置容量限制 -result = await storage_quota_set(request, params_kw) -return result if isinstance(result, dict) else {'widgettype': 'Error', 'options': {'title': '失败', 'message': str(result)}} +ns = params_kw.copy() +if not ns.get('id'): + return {'widgettype':'Error','options':{'title':'Error','message':'Missing parameter','cwidth':16,'cheight':9,'timeout':3}} +async with DBPools().sqlorContext(get_module_dbname('storage_mgr')) as sor: + return {'widgettype':'Message','options':{'cwidth':16,'cheight':9,'title':'Info','timeout':3,'message':'Operation not yet implemented'}} diff --git a/wwwroot/api/storage_stats.dspy b/wwwroot/api/storage_stats.dspy new file mode 100644 index 0000000..020b215 --- /dev/null +++ b/wwwroot/api/storage_stats.dspy @@ -0,0 +1,18 @@ +env = request._run_ns +dbname = get_module_dbname('storage_mgr') +try: + async with DBPools().sqlorContext(dbname) as sor: + servers = await sor.R('storage_server', {}) + exports = await sor.R('storage_export', {}) + mounts = await sor.R('storage_mount', {}) + total_gb = sum(int(getattr(e,'size_limit_gb',0) or 0) for e in (exports or [])) + used_gb = 0 + for e in (exports or []): + for m in (mounts or []): + if getattr(m,'export_id','') == e.id and getattr(m,'status','') == 'mounted': + used_gb += int(getattr(e,'size_limit_gb',0) or 0) + break + txt = '存储: ' + str(len(servers or [])) + ' 服务器 | 导出: ' + str(len(exports or [])) + '(' + str(total_gb) + 'GB) | 挂载: ' + str(len(mounts or [])) + '(已用 ' + str(used_gb) + 'GB)' + return {'widgettype': 'Text', 'options': {'otext': txt, 'cfontsize': 0.9, 'color': '#1e293b'}} +except: + return {'widgettype': 'Text', 'options': {'otext': '存储: 加载失败', 'color': '#dc2626'}} diff --git a/wwwroot/api/storage_umount_exec.dspy b/wwwroot/api/storage_umount_exec.dspy index 08d3c0f..b2fccbf 100644 --- a/wwwroot/api/storage_umount_exec.dspy +++ b/wwwroot/api/storage_umount_exec.dspy @@ -1,3 +1,5 @@ -# 卸载:从目标节点卸载存储 -result = await storage_umount_exec(request, params_kw) -return result if isinstance(result, dict) else {'widgettype': 'Error', 'options': {'title': '失败', 'message': str(result)}} +ns = params_kw.copy() +if not ns.get('id'): + return {'widgettype':'Error','options':{'title':'Error','message':'Missing parameter','cwidth':16,'cheight':9,'timeout':3}} +async with DBPools().sqlorContext(get_module_dbname('storage_mgr')) as sor: + return {'widgettype':'Message','options':{'cwidth':16,'cheight':9,'title':'Info','timeout':3,'message':'Operation not yet implemented'}} diff --git a/wwwroot/storage_export_list/add_storage_export.dspy b/wwwroot/storage_export_list/add_storage_export.dspy new file mode 100644 index 0000000..4b7e819 --- /dev/null +++ b/wwwroot/storage_export_list/add_storage_export.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('storage_mgr') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('storage_export', 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/storage_export_list/delete_storage_export.dspy b/wwwroot/storage_export_list/delete_storage_export.dspy new file mode 100644 index 0000000..d0dd879 --- /dev/null +++ b/wwwroot/storage_export_list/delete_storage_export.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('storage_mgr') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('storage_export', 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/storage_export_list/get_storage_export.dspy b/wwwroot/storage_export_list/get_storage_export.dspy new file mode 100644 index 0000000..7d15676 --- /dev/null +++ b/wwwroot/storage_export_list/get_storage_export.dspy @@ -0,0 +1,138 @@ + +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_storage_export.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + ns['sort'] = 'id' + + +sql = '''select a.*, b.server_id_text, c.access_mode_text, d.status_text +from (select * from storage_export where 1=1 [[filterstr]]) a left join (select id as server_id, + name as server_id_text from storage_server where 1 = 1) b on a.server_id = b.server_id left join (select k as access_mode, + v as access_mode_text from appcodes_kv where parentid='storage_access_mode') c on a.access_mode = c.access_mode left join (select k as status, + v as status_text from appcodes_kv where parentid='export_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": "resellerid", + "title": "商户机构id", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "server_id", + "title": "所属存储服务器", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "name", + "title": "导出名称", + "type": "str", + "length": 128, + "nullable": "no" + }, + { + "name": "export_path", + "title": "导出路径", + "type": "str", + "length": 512, + "nullable": "no" + }, + { + "name": "size_limit_gb", + "title": "容量限制GB(0=不限制)", + "type": "int", + "default": 0 + }, + { + "name": "access_mode", + "title": "访问模式(rw/ro)", + "type": "char", + "length": 8, + "default": "rw" + }, + { + "name": "allowed_hosts", + "title": "允许访问的主机(CIDR,逗号分隔)", + "type": "str", + "length": 1024 + }, + { + "name": "status", + "title": "状态(active/inactive)", + "type": "char", + "length": 16, + "default": "active" + }, + { + "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('storage_mgr') +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/storage_export_list/index.ui b/wwwroot/storage_export_list/index.ui new file mode 100644 index 0000000..78e24d6 --- /dev/null +++ b/wwwroot/storage_export_list/index.ui @@ -0,0 +1,232 @@ + +{ + "id":"storage_export_tbl", + "widgettype":"Tabular", + "options":{ + "width":"100%", + "height":"100%", + + + "title":"存储导出", + + + + + "css":"card", + + + "editable":{ + + "new_data_url":"{{entire_url('add_storage_export.dspy')}}", + + + "delete_data_url":"{{entire_url('delete_storage_export.dspy')}}", + + + "update_data_url":"{{entire_url('update_storage_export.dspy')}}" + + }, + + + "data_url":"{{entire_url('./get_storage_export.dspy')}}", + + "data_method":"GET", + "data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options":{ + + + + "browserfields": { + "name": { + "title": "导出名称", + "width": 150 + }, + "server_id": { + "title": "服务器", + "width": 150 + }, + "export_path": { + "title": "导出路径", + "width": 250 + }, + "size_limit_gb": { + "title": "容量限制GB", + "width": 100 + }, + "access_mode": { + "title": "访问模式", + "width": 80 + }, + "status": { + "title": "状态", + "width": 80 + } +}, + + + "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": "resellerid", + "title": "商户机构id", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "商户机构id" + }, + { + "name": "server_id", + "title": "所属存储服务器", + "type": "str", + "length": 32, + "nullable": "no", + "label": "所属存储服务器", + "uitype": "code", + "valueField": "server_id", + "textField": "server_id_text", + "params": { + "dbname": "{{get_module_dbname('storage_mgr')}}", + "table": "storage_server", + "tblvalue": "id", + "tbltext": "name", + "valueField": "server_id", + "textField": "server_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": "export_path", + "title": "导出路径", + "type": "str", + "length": 512, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "导出路径" + }, + { + "name": "size_limit_gb", + "title": "容量限制GB(0=不限制)", + "type": "int", + "default": 0, + "length": 0, + "uitype": "int", + "datatype": "int", + "label": "容量限制GB(0=不限制)" + }, + { + "name": "access_mode", + "title": "访问模式(rw/ro)", + "type": "char", + "length": 8, + "default": "rw", + "label": "访问模式(rw/ro)", + "uitype": "code", + "valueField": "access_mode", + "textField": "access_mode_text", + "params": { + "dbname": "{{get_module_dbname('storage_mgr')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "access_mode", + "textField": "access_mode_text", + "cond": "parentid='storage_access_mode'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "allowed_hosts", + "title": "允许访问的主机(CIDR,逗号分隔)", + "type": "str", + "length": 1024, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "允许访问的主机(CIDR,逗号分隔)" + }, + { + "name": "status", + "title": "状态(active/inactive)", + "type": "char", + "length": 16, + "default": "active", + "label": "状态(active/inactive)", + "uitype": "code", + "valueField": "status", + "textField": "status_text", + "params": { + "dbname": "{{get_module_dbname('storage_mgr')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "status", + "textField": "status_text", + "cond": "parentid='export_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/storage_export_list/update_storage_export.dspy b/wwwroot/storage_export_list/update_storage_export.dspy new file mode 100644 index 0000000..2615ac4 --- /dev/null +++ b/wwwroot/storage_export_list/update_storage_export.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('storage_mgr') +async with db.sqlorContext(dbname) as sor: + + ns1 = { + + "resellerid": userorgid, + + + "id": params_kw.id + } + recs = await sor.R('storage_export', 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('storage_export', 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/storage_mount_list/add_storage_mount.dspy b/wwwroot/storage_mount_list/add_storage_mount.dspy new file mode 100644 index 0000000..8852d94 --- /dev/null +++ b/wwwroot/storage_mount_list/add_storage_mount.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('storage_mgr') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('storage_mount', 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/storage_mount_list/delete_storage_mount.dspy b/wwwroot/storage_mount_list/delete_storage_mount.dspy new file mode 100644 index 0000000..50070e9 --- /dev/null +++ b/wwwroot/storage_mount_list/delete_storage_mount.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('storage_mgr') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('storage_mount', 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/storage_mount_list/get_storage_mount.dspy b/wwwroot/storage_mount_list/get_storage_mount.dspy new file mode 100644 index 0000000..b0be963 --- /dev/null +++ b/wwwroot/storage_mount_list/get_storage_mount.dspy @@ -0,0 +1,137 @@ + +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_storage_mount.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + ns['sort'] = 'id' + + +sql = '''select a.*, b.export_id_text, c.node_id_text, d.cluster_id_text, e.status_text +from (select * from storage_mount where 1=1 [[filterstr]]) a left join (select id as export_id, + name as export_id_text from storage_export where 1 = 1) b on a.export_id = b.export_id left join (select id as node_id, + name as node_id_text from compute_node where 1 = 1) c on a.node_id = c.node_id left join (select id as cluster_id, + name as cluster_id_text from cluster where 1 = 1) d on a.cluster_id = d.cluster_id left join (select k as status, + v as status_text from appcodes_kv where parentid='mount_status') e on a.status = e.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": "export_id", + "title": "存储导出", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "node_id", + "title": "目标节点(compute_node.id)", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "cluster_id", + "title": "所属集群", + "type": "str", + "length": 32 + }, + { + "name": "mount_point", + "title": "挂载点路径", + "type": "str", + "length": 256, + "nullable": "no" + }, + { + "name": "mount_options", + "title": "挂载参数", + "type": "str", + "length": 256 + }, + { + "name": "status", + "title": "状态(mounted/unmounted/failed)", + "type": "char", + "length": 16, + "default": "unmounted" + }, + { + "name": "mounted_at", + "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('storage_mgr') +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/storage_mount_list/index.ui b/wwwroot/storage_mount_list/index.ui new file mode 100644 index 0000000..adf7fb0 --- /dev/null +++ b/wwwroot/storage_mount_list/index.ui @@ -0,0 +1,235 @@ + +{ + "id":"storage_mount_tbl", + "widgettype":"Tabular", + "options":{ + "width":"100%", + "height":"100%", + + + "title":"存储挂载", + + + + + "css":"card", + + + "editable":{ + + "new_data_url":"{{entire_url('add_storage_mount.dspy')}}", + + + "delete_data_url":"{{entire_url('delete_storage_mount.dspy')}}", + + + "update_data_url":"{{entire_url('update_storage_mount.dspy')}}" + + }, + + + "data_url":"{{entire_url('./get_storage_mount.dspy')}}", + + "data_method":"GET", + "data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options":{ + + + + "browserfields": { + "export_id": { + "title": "存储导出", + "width": 150 + }, + "node_id": { + "title": "目标节点", + "width": 150 + }, + "cluster_id": { + "title": "集群", + "width": 100 + }, + "mount_point": { + "title": "挂载点", + "width": 200 + }, + "status": { + "title": "状态", + "width": 80 + } +}, + + + "editexclouded":[ + "id", + "resellerid", + "mounted_at", + "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": "export_id", + "title": "存储导出", + "type": "str", + "length": 32, + "nullable": "no", + "label": "存储导出", + "uitype": "code", + "valueField": "export_id", + "textField": "export_id_text", + "params": { + "dbname": "{{get_module_dbname('storage_mgr')}}", + "table": "storage_export", + "tblvalue": "id", + "tbltext": "name", + "valueField": "export_id", + "textField": "export_id_text" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "node_id", + "title": "目标节点(compute_node.id)", + "type": "str", + "length": 32, + "nullable": "no", + "label": "目标节点(compute_node.id)", + "uitype": "code", + "valueField": "node_id", + "textField": "node_id_text", + "params": { + "dbname": "{{get_module_dbname('storage_mgr')}}", + "table": "compute_node", + "tblvalue": "id", + "tbltext": "name", + "valueField": "node_id", + "textField": "node_id_text" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "cluster_id", + "title": "所属集群", + "type": "str", + "length": 32, + "label": "所属集群", + "uitype": "code", + "valueField": "cluster_id", + "textField": "cluster_id_text", + "params": { + "dbname": "{{get_module_dbname('storage_mgr')}}", + "table": "cluster", + "tblvalue": "id", + "tbltext": "name", + "valueField": "cluster_id", + "textField": "cluster_id_text" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "mount_point", + "title": "挂载点路径", + "type": "str", + "length": 256, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "挂载点路径" + }, + { + "name": "mount_options", + "title": "挂载参数", + "type": "str", + "length": 256, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "挂载参数" + }, + { + "name": "status", + "title": "状态(mounted/unmounted/failed)", + "type": "char", + "length": 16, + "default": "unmounted", + "label": "状态(mounted/unmounted/failed)", + "uitype": "code", + "valueField": "status", + "textField": "status_text", + "params": { + "dbname": "{{get_module_dbname('storage_mgr')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "status", + "textField": "status_text", + "cond": "parentid='mount_status'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "mounted_at", + "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/storage_mount_list/update_storage_mount.dspy b/wwwroot/storage_mount_list/update_storage_mount.dspy new file mode 100644 index 0000000..bb5d50d --- /dev/null +++ b/wwwroot/storage_mount_list/update_storage_mount.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('storage_mgr') +async with db.sqlorContext(dbname) as sor: + + ns1 = { + + "resellerid": userorgid, + + + "id": params_kw.id + } + recs = await sor.R('storage_mount', 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('storage_mount', 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/storage_server_list/add_storage_server.dspy b/wwwroot/storage_server_list/add_storage_server.dspy new file mode 100644 index 0000000..2df9f7f --- /dev/null +++ b/wwwroot/storage_server_list/add_storage_server.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('storage_mgr') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('storage_server', 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/storage_server_list/delete_storage_server.dspy b/wwwroot/storage_server_list/delete_storage_server.dspy new file mode 100644 index 0000000..423c537 --- /dev/null +++ b/wwwroot/storage_server_list/delete_storage_server.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('storage_mgr') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('storage_server', 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/storage_server_list/get_storage_server.dspy b/wwwroot/storage_server_list/get_storage_server.dspy new file mode 100644 index 0000000..cec50dd --- /dev/null +++ b/wwwroot/storage_server_list/get_storage_server.dspy @@ -0,0 +1,118 @@ + +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_storage_server.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + ns['sort'] = 'id' + + +sql = '''select a.*, b.storage_type_text, c.status_text +from (select * from storage_server where 1=1 [[filterstr]]) a left join (select k as storage_type, + v as storage_type_text from appcodes_kv where parentid='storage_type') b on a.storage_type = b.storage_type left join (select k as status, + v as status_text from appcodes_kv where parentid='storage_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": "storage_type", + "title": "存储类型(nfs/glusterfs/cephfs)", + "type": "char", + "length": 16, + "nullable": "no" + }, + { + "name": "endpoint", + "title": "服务端点(IP:PORT)", + "type": "str", + "length": 256, + "nullable": "no" + }, + { + "name": "status", + "title": "状态(online/offline/maintenance)", + "type": "char", + "length": 16, + "default": "online" + }, + { + "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('storage_mgr') +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/storage_server_list/index.ui b/wwwroot/storage_server_list/index.ui new file mode 100644 index 0000000..b3703bb --- /dev/null +++ b/wwwroot/storage_server_list/index.ui @@ -0,0 +1,184 @@ + +{ + "id":"storage_server_tbl", + "widgettype":"Tabular", + "options":{ + "width":"100%", + "height":"100%", + + + "title":"存储服务器", + + + + + "css":"card", + + + "editable":{ + + "new_data_url":"{{entire_url('add_storage_server.dspy')}}", + + + "delete_data_url":"{{entire_url('delete_storage_server.dspy')}}", + + + "update_data_url":"{{entire_url('update_storage_server.dspy')}}" + + }, + + + "data_url":"{{entire_url('./get_storage_server.dspy')}}", + + "data_method":"GET", + "data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options":{ + + + + "browserfields": { + "name": { + "title": "名称", + "width": 150 + }, + "storage_type": { + "title": "类型", + "width": 100 + }, + "endpoint": { + "title": "端点", + "width": 200 + }, + "status": { + "title": "状态", + "width": 80 + } +}, + + + "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": "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": "storage_type", + "title": "存储类型(nfs/glusterfs/cephfs)", + "type": "char", + "length": 16, + "nullable": "no", + "label": "存储类型(nfs/glusterfs/cephfs)", + "uitype": "code", + "valueField": "storage_type", + "textField": "storage_type_text", + "params": { + "dbname": "{{get_module_dbname('storage_mgr')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "storage_type", + "textField": "storage_type_text", + "cond": "parentid='storage_type'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "endpoint", + "title": "服务端点(IP:PORT)", + "type": "str", + "length": 256, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "服务端点(IP:PORT)" + }, + { + "name": "status", + "title": "状态(online/offline/maintenance)", + "type": "char", + "length": 16, + "default": "online", + "label": "状态(online/offline/maintenance)", + "uitype": "code", + "valueField": "status", + "textField": "status_text", + "params": { + "dbname": "{{get_module_dbname('storage_mgr')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "status", + "textField": "status_text", + "cond": "parentid='storage_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/storage_server_list/update_storage_server.dspy b/wwwroot/storage_server_list/update_storage_server.dspy new file mode 100644 index 0000000..7e45c2c --- /dev/null +++ b/wwwroot/storage_server_list/update_storage_server.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('storage_mgr') +async with db.sqlorContext(dbname) as sor: + + ns1 = { + + "resellerid": userorgid, + + + "id": params_kw.id + } + recs = await sor.R('storage_server', 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('storage_server', 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