feat: menu/i18n/CRUD/permission/stats/model fixes
This commit is contained in:
parent
2efc52b737
commit
c2023174ab
@ -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,
|
||||||
|
}}
|
||||||
|
|
||||||
@ -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.")
|
||||||
@ -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,
|
||||||
|
}}
|
||||||
|
|
||||||
70
build/lib/build/lib/build/lib/build/lib/scripts/load_path.py
Normal file
70
build/lib/build/lib/build/lib/build/lib/scripts/load_path.py
Normal file
@ -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.")
|
||||||
208
build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py
Normal file
208
build/lib/build/lib/build/lib/build/lib/storage_mgr/__init__.py
Normal file
@ -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,
|
||||||
|
}}
|
||||||
|
|
||||||
70
build/lib/build/lib/build/lib/scripts/load_path.py
Normal file
70
build/lib/build/lib/build/lib/scripts/load_path.py
Normal file
@ -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.")
|
||||||
208
build/lib/build/lib/build/lib/storage_mgr/__init__.py
Normal file
208
build/lib/build/lib/build/lib/storage_mgr/__init__.py
Normal file
@ -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,
|
||||||
|
}}
|
||||||
|
|
||||||
70
build/lib/build/lib/scripts/load_path.py
Normal file
70
build/lib/build/lib/scripts/load_path.py
Normal file
@ -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.")
|
||||||
208
build/lib/build/lib/storage_mgr/__init__.py
Normal file
208
build/lib/build/lib/storage_mgr/__init__.py
Normal file
@ -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,
|
||||||
|
}}
|
||||||
|
|
||||||
70
build/lib/scripts/load_path.py
Normal file
70
build/lib/scripts/load_path.py
Normal file
@ -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.")
|
||||||
208
build/lib/storage_mgr/__init__.py
Normal file
208
build/lib/storage_mgr/__init__.py
Normal file
@ -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,
|
||||||
|
}}
|
||||||
|
|
||||||
36
i18n/en/msg.txt
Normal file
36
i18n/en/msg.txt
Normal file
@ -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
|
||||||
36
i18n/zh/msg.txt
Normal file
36
i18n/zh/msg.txt
Normal file
@ -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)
|
||||||
|
集群: 集群
|
||||||
@ -4,19 +4,43 @@
|
|||||||
"title": "存储导出",
|
"title": "存储导出",
|
||||||
"params": {
|
"params": {
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"name": {"title": "导出名称", "width": 150},
|
"name": {
|
||||||
"server_id": {"title": "服务器", "width": 150},
|
"title": "导出名称",
|
||||||
"export_path": {"title": "导出路径", "width": 250},
|
"width": 150
|
||||||
"size_limit_gb": {"title": "容量限制GB", "width": 100},
|
},
|
||||||
"access_mode": {"title": "访问模式", "width": 80},
|
"server_id": {
|
||||||
"status": {"title": "状态", "width": 80}
|
"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"],
|
"editexclouded": [
|
||||||
"toolbar": {"tools": []},
|
"id",
|
||||||
|
"resellerid",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
],
|
||||||
"binds": [],
|
"binds": [],
|
||||||
"new_data_url": "{{entire_url('/storage_mgr/api/storage_export_create.dspy')}}",
|
"logined_userorgid": "resellerid",
|
||||||
"update_data_url": "{{entire_url('/storage_mgr/api/storage_export_update.dspy')}}",
|
"editable": {
|
||||||
"delete_data_url": "{{entire_url('/storage_mgr/api/storage_export_delete.dspy')}}",
|
"new_data_url": "{{entire_url('/storage_mgr/api/storage_export_create.dspy')}}",
|
||||||
"logined_userorgid": "resellerid"
|
"update_data_url": "{{entire_url('/storage_mgr/api/storage_export_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('/storage_mgr/api/storage_export_delete.dspy')}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,18 +4,40 @@
|
|||||||
"title": "存储挂载",
|
"title": "存储挂载",
|
||||||
"params": {
|
"params": {
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"export_id": {"title": "存储导出", "width": 150},
|
"export_id": {
|
||||||
"node_id": {"title": "目标节点", "width": 150},
|
"title": "存储导出",
|
||||||
"cluster_id": {"title": "集群", "width": 100},
|
"width": 150
|
||||||
"mount_point": {"title": "挂载点", "width": 200},
|
},
|
||||||
"status": {"title": "状态", "width": 80}
|
"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"],
|
"editexclouded": [
|
||||||
"toolbar": {"tools": []},
|
"id",
|
||||||
|
"resellerid",
|
||||||
|
"mounted_at",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
],
|
||||||
"binds": [],
|
"binds": [],
|
||||||
"new_data_url": "{{entire_url('/storage_mgr/api/storage_mount_create.dspy')}}",
|
"logined_userorgid": "resellerid",
|
||||||
"update_data_url": "{{entire_url('/storage_mgr/api/storage_mount_update.dspy')}}",
|
"editable": {
|
||||||
"delete_data_url": "{{entire_url('/storage_mgr/api/storage_mount_delete.dspy')}}",
|
"new_data_url": "{{entire_url('/storage_mgr/api/storage_mount_create.dspy')}}",
|
||||||
"logined_userorgid": "resellerid"
|
"update_data_url": "{{entire_url('/storage_mgr/api/storage_mount_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('/storage_mgr/api/storage_mount_delete.dspy')}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,19 +4,35 @@
|
|||||||
"title": "存储服务器",
|
"title": "存储服务器",
|
||||||
"params": {
|
"params": {
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"name": {"title": "名称", "width": 150},
|
"name": {
|
||||||
"storage_type": {"title": "类型", "width": 100},
|
"title": "名称",
|
||||||
"endpoint": {"title": "端点", "width": 200},
|
"width": 150
|
||||||
"total_capacity_gb": {"title": "总容量GB", "width": 100},
|
},
|
||||||
"used_capacity_gb": {"title": "已用GB", "width": 80},
|
"storage_type": {
|
||||||
"status": {"title": "状态", "width": 80}
|
"title": "类型",
|
||||||
|
"width": 100
|
||||||
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"title": "端点",
|
||||||
|
"width": 200
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"title": "状态",
|
||||||
|
"width": 80
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"editexclouded": ["id", "resellerid", "created_at", "updated_at"],
|
"editexclouded": [
|
||||||
"toolbar": {"tools": []},
|
"id",
|
||||||
|
"resellerid",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
],
|
||||||
"binds": [],
|
"binds": [],
|
||||||
"new_data_url": "{{entire_url('/storage_mgr/api/storage_server_create.dspy')}}",
|
"logined_userorgid": "resellerid",
|
||||||
"update_data_url": "{{entire_url('/storage_mgr/api/storage_server_update.dspy')}}",
|
"editable": {
|
||||||
"delete_data_url": "{{entire_url('/storage_mgr/api/storage_server_delete.dspy')}}",
|
"new_data_url": "{{entire_url('/storage_mgr/api/storage_server_create.dspy')}}",
|
||||||
"logined_userorgid": "resellerid"
|
"update_data_url": "{{entire_url('/storage_mgr/api/storage_server_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('/storage_mgr/api/storage_server_delete.dspy')}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,24 +1,84 @@
|
|||||||
{
|
{
|
||||||
"summary": [{
|
"summary": [
|
||||||
"name": "storage_server",
|
{
|
||||||
"title": "存储服务器",
|
"name": "storage_server",
|
||||||
"primary": ["id"],
|
"title": "存储服务器",
|
||||||
"catelog": "entity"
|
"primary": [
|
||||||
}],
|
"id"
|
||||||
|
],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "id", "title": "id", "type": "str", "length": 32, "nullable": "no"},
|
{
|
||||||
{"name": "resellerid", "title": "商户机构id", "type": "str", "length": 32, "nullable": "no"},
|
"name": "id",
|
||||||
{"name": "name", "title": "服务器名称", "type": "str", "length": 128, "nullable": "no"},
|
"title": "id",
|
||||||
{"name": "storage_type", "title": "存储类型(nfs/glusterfs/cephfs)", "type": "char", "length": 16, "nullable": "no"},
|
"type": "str",
|
||||||
{"name": "endpoint", "title": "服务端点(IP:PORT)", "type": "str", "length": 256, "nullable": "no"},
|
"length": 32,
|
||||||
{"name": "total_capacity_gb", "title": "总容量GB", "type": "int", "default": 0},
|
"nullable": "no"
|
||||||
{"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": "resellerid",
|
||||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
"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": [
|
"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'"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@ -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 = ["."]
|
||||||
9
storage_mgr.egg-info/PKG-INFO
Normal file
9
storage_mgr.egg-info/PKG-INFO
Normal file
@ -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
|
||||||
9
storage_mgr.egg-info/SOURCES.txt
Normal file
9
storage_mgr.egg-info/SOURCES.txt
Normal file
@ -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
|
||||||
1
storage_mgr.egg-info/dependency_links.txt
Normal file
1
storage_mgr.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
4
storage_mgr.egg-info/requires.txt
Normal file
4
storage_mgr.egg-info/requires.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
apppublic
|
||||||
|
sqlor
|
||||||
|
ahserver
|
||||||
|
appbase
|
||||||
7
storage_mgr.egg-info/top_level.txt
Normal file
7
storage_mgr.egg-info/top_level.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
build
|
||||||
|
i18n
|
||||||
|
json
|
||||||
|
models
|
||||||
|
scripts
|
||||||
|
storage_mgr
|
||||||
|
wwwroot
|
||||||
@ -186,3 +186,23 @@ async def storage_quota_set(request, params_kw):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
return {'status': 'ok', 'message': 'Quota set: ' + str(size_limit_gb) + 'GB on export ' + exp.name}
|
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,
|
||||||
|
}}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
# 执行挂载:将存储导出挂载到目标节点
|
ns = params_kw.copy()
|
||||||
result = await storage_mount_exec(request, params_kw)
|
if not ns.get('id'):
|
||||||
return result if isinstance(result, dict) else {'widgettype': 'Error', 'options': {'title': '失败', 'message': str(result)}}
|
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'}}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
# 设置存储配额:对导出路径设置容量限制
|
ns = params_kw.copy()
|
||||||
result = await storage_quota_set(request, params_kw)
|
if not ns.get('id'):
|
||||||
return result if isinstance(result, dict) else {'widgettype': 'Error', 'options': {'title': '失败', 'message': str(result)}}
|
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'}}
|
||||||
|
|||||||
18
wwwroot/api/storage_stats.dspy
Normal file
18
wwwroot/api/storage_stats.dspy
Normal file
@ -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'}}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
# 卸载:从目标节点卸载存储
|
ns = params_kw.copy()
|
||||||
result = await storage_umount_exec(request, params_kw)
|
if not ns.get('id'):
|
||||||
return result if isinstance(result, dict) else {'widgettype': 'Error', 'options': {'title': '失败', 'message': str(result)}}
|
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'}}
|
||||||
|
|||||||
57
wwwroot/storage_export_list/add_storage_export.dspy
Normal file
57
wwwroot/storage_export_list/add_storage_export.dspy
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
47
wwwroot/storage_export_list/delete_storage_export.dspy
Normal file
47
wwwroot/storage_export_list/delete_storage_export.dspy
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
138
wwwroot/storage_export_list/get_storage_export.dspy
Normal file
138
wwwroot/storage_export_list/get_storage_export.dspy
Normal file
@ -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":[]
|
||||||
|
}
|
||||||
232
wwwroot/storage_export_list/index.ui
Normal file
232
wwwroot/storage_export_list/index.ui
Normal file
@ -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":[]
|
||||||
|
|
||||||
|
}
|
||||||
75
wwwroot/storage_export_list/update_storage_export.dspy
Normal file
75
wwwroot/storage_export_list/update_storage_export.dspy
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
57
wwwroot/storage_mount_list/add_storage_mount.dspy
Normal file
57
wwwroot/storage_mount_list/add_storage_mount.dspy
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
47
wwwroot/storage_mount_list/delete_storage_mount.dspy
Normal file
47
wwwroot/storage_mount_list/delete_storage_mount.dspy
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
137
wwwroot/storage_mount_list/get_storage_mount.dspy
Normal file
137
wwwroot/storage_mount_list/get_storage_mount.dspy
Normal file
@ -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":[]
|
||||||
|
}
|
||||||
235
wwwroot/storage_mount_list/index.ui
Normal file
235
wwwroot/storage_mount_list/index.ui
Normal file
@ -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":[]
|
||||||
|
|
||||||
|
}
|
||||||
75
wwwroot/storage_mount_list/update_storage_mount.dspy
Normal file
75
wwwroot/storage_mount_list/update_storage_mount.dspy
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
57
wwwroot/storage_server_list/add_storage_server.dspy
Normal file
57
wwwroot/storage_server_list/add_storage_server.dspy
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
47
wwwroot/storage_server_list/delete_storage_server.dspy
Normal file
47
wwwroot/storage_server_list/delete_storage_server.dspy
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
118
wwwroot/storage_server_list/get_storage_server.dspy
Normal file
118
wwwroot/storage_server_list/get_storage_server.dspy
Normal file
@ -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":[]
|
||||||
|
}
|
||||||
184
wwwroot/storage_server_list/index.ui
Normal file
184
wwwroot/storage_server_list/index.ui
Normal file
@ -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":[]
|
||||||
|
|
||||||
|
}
|
||||||
75
wwwroot/storage_server_list/update_storage_server.dspy
Normal file
75
wwwroot/storage_server_list/update_storage_server.dspy
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user