Files
2026-04-07 02:04:22 +05:30

462 lines
16 KiB
Python

import base64
import json
import os.path
import re
import time
import sys
from urllib.parse import urlparse
from dataclasses import dataclass, field
from typing import Tuple, Optional, List, Union, Dict
if "/www/server/panel/class" not in sys.path:
sys.path.insert(0, "/www/server/panel/class")
import public
import db
if "/www/server/panel" not in sys.path:
sys.path.insert(0, "/www/server/panel")
@dataclass
class NodeAPPKey:
origin: str
request_token: str
app_key: str
app_token: str
def to_string(self)->str:
data = "|".join((self.origin, self.request_token, self.app_key, self.app_token))
return base64.b64encode(data.encode()).decode("utf-8")
@dataclass
class Node:
remarks: str
id: int = 0
address: str = ""
category_id: int = 0
api_key: str = ""
create_time: int = 0
server_ip: str = ""
status: int = 1
error: dict = field(default_factory=dict)
error_num: int = 0
app_key: str = ""
ssh_conf: dict = field(default_factory=dict)
lpver: str = ""
@classmethod
def from_dict(cls, data: dict) -> Tuple[Optional["Node"], str]:
if not isinstance(data.get('remarks', None), str):
return None, 'remarks is required'
if not data["remarks"].strip():
return None, 'remarks is required'
data["remarks"] = data["remarks"].strip()
api_key = data.get('api_key', '')
app_key = data.get('app_key', '')
ssh_conf: dict = data.get('ssh_conf', {})
if not api_key and not app_key and not ssh_conf:
return None, 'api_key or app_key or ssh_conf is required'
if app_key:
app = cls.parse_app_key(app_key)
if not app:
return None, 'App_key format error'
data["address"] = app.origin
url = urlparse(data["address"], allow_fragments=False)
if not url.scheme or not url.netloc:
return None, 'address is invalid'
if api_key:
if not isinstance(data.get('address', None), str):
return None, 'address is required'
url = urlparse(data["address"], allow_fragments=False)
if not url.scheme or not url.netloc:
return None, 'address is invalid'
if ssh_conf:
for key in ("host", "port"):
if key not in ssh_conf:
return None, 'ssh_conf is invalid'
if "username" not in ssh_conf:
ssh_conf["username"] = "root"
if "password" not in ssh_conf:
ssh_conf["password"] = ""
if "pkey" not in ssh_conf:
ssh_conf["pkey"] = ""
if "pkey_passwd" not in ssh_conf:
ssh_conf["pkey_passwd"] = ""
if ssh_conf and not data.get("address", None):
data["address"] = ssh_conf["host"]
n = Node(
data["remarks"], id=data.get('id', 0), address=data.get("address"), category_id=int(data.get('category_id', 0)),
api_key=api_key, create_time=data.get('create_time', 0), server_ip=data.get('server_ip', ''),
status=data.get('status', 1), error=data.get('error', {}), error_num=data.get('error_num', 0),
app_key=app_key, ssh_conf=ssh_conf, lpver=data.get('lpver', '')
)
return n, ''
def to_dict(self) -> dict:
return {
"remarks": self.remarks,
"id": self.id,
"address": self.address,
"category_id": self.category_id,
"api_key": self.api_key,
"create_time": self.create_time,
"server_ip": self.server_ip,
"status": self.status,
"error": self.error,
"error_num": self.error_num,
"app_key": self.app_key,
"ssh_conf": self.ssh_conf,
"lpver": self.lpver
}
def parse_server_ip(self):
import socket
from urllib.parse import urlparse
if not self.address.startswith("http"):
host = self.address # 仅 ssh时 address本身就是host
else:
host = urlparse(self.address).hostname
if isinstance(host, str) and public.check_ip(host):
return host
try:
ip_address = socket.gethostbyname(host)
return ip_address
except socket.gaierror as e:
public.print_log(f"Error: {e}")
return ""
@staticmethod
def parse_app_key(app_key: str) -> Optional[NodeAPPKey]:
try:
data = base64.b64decode(app_key).decode("utf-8")
origin, request_token, app_key, app_token = data.split("|")
origin_arr = origin.split(":")
if len(origin_arr) > 3:
origin = ":".join(origin_arr[:3])
return NodeAPPKey(origin, request_token, app_key, app_token)
except:
return None
class ServerNodeDB:
_DB_FILE = public.get_panel_path() + "/data/db/node.db"
_DB_INIT_FILE = os.path.dirname(__file__) + "/node.sql"
def __init__(self):
sql = db.Sql()
sql._Sql__DB_FILE = self._DB_FILE
self.db = sql
def init_db(self):
sql_data = public.readFile(self._DB_INIT_FILE)
import sqlite3
conn = sqlite3.connect(self._DB_FILE)
cur = conn.cursor()
cur.executescript(sql_data)
cur.execute("PRAGMA table_info(node)")
existing_cols = [row[1] for row in cur.fetchall()]
if "ssh_test" in existing_cols:
pass
# print("字段 ssh_test 已存在")
else:
cur.execute("ALTER TABLE node ADD COLUMN ssh_test INTEGER DEFAULT (0)")
conn.commit()
conn.close()
def close(self):
self.db.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_trackback):
self.close()
def __del__(self):
self.close()
def is_local_node(self, node_id: int):
return self.db.table('node').where("id=? AND app_key = 'local' AND api_key = 'local'", (node_id,)).count() > 0
def get_local_node(self):
data = self.db.table('node').where("app_key = 'local' AND api_key = 'local'", ()).find()
if isinstance(data, dict):
return data
return {
"id": 0,
"address": "",
"category_id": 0,
"remarks": "Local node",
"api_key": "local",
"create_time": time.strftime('%Y-%m-%d %H:%M:%S'),
"server_ip": "127.0.0.1",
"status": 0,
"error": 0,
"error_num": 0,
"app_key": "local",
"ssh_conf": "{}",
"lpver": "",
}
def create_node(self, node: Node) -> str:
node_data = node.to_dict()
node_data.pop("id")
node_data["create_time"] = time.strftime('%Y-%m-%d %H:%M:%S')
node_data.pop("error")
node_data["status"] = 1
node_data["ssh_conf"] = json.dumps(node_data["ssh_conf"])
if node.category_id > 0 and not self.category_exites(node.category_id):
return "Classification does not exist"
if self.db.table('node').where('remarks=?', (node.remarks,)).count() > 0:
return "The node with this name already exists"
try:
node_id = self.db.table('node').insert(node_data)
if isinstance(node_id, int):
node.id = node_id
return ""
elif isinstance(node_id, str):
return node_id
else:
return str(node_id)
except Exception as e:
return str(e)
def update_node(self, node: Node, with_out_fields: List[str] = Node) -> str:
if self.is_local_node(node.id):
return "Cannot modify local nodes"
if not self.node_id_exites(node.id):
return "Node does not exist"
node_data = node.to_dict()
node_data.pop("create_time")
node_data.pop("id")
node_data["ssh_conf"] = json.dumps(node_data["ssh_conf"])
node_data["error"] = json.dumps(node_data["error"])
if with_out_fields and isinstance(with_out_fields, list):
for f in with_out_fields:
if f in node_data:
node_data.pop(f)
if node.category_id > 0 and not self.category_exites(node.category_id):
node.category_id = 0
node_data["category_id"] = 0
try:
res = self.db.table('node').where('id=?', (node.id,)).update(node_data)
if isinstance(res, str):
return res
except Exception as e:
return str(e)
return ""
def set_node_ssh_conf(self, node_id: int, ssh_conf: dict, ssh_test: int=0):
pdata = {"ssh_conf": json.dumps(ssh_conf)}
if ssh_test:
pdata["ssh_test"] = 1
self.db.table('node').where('id=?', (node_id,)).update(pdata)
return
def remove_node_ssh_conf(self, node_id: int):
self.db.table('node').where('id=?', (node_id,)).update({"ssh_conf": "{}"})
return
def delete_node(self, node_id: int) -> str:
if self.is_local_node(node_id):
return "Cannot delete local node"
if not self.node_id_exites(node_id):
return "Node does not exist"
try:
res = self.db.table('node').where('id=?', (node_id,)).delete()
if isinstance(res, str):
return res
except Exception as e:
return str(e)
return ""
def find_node(self, api_key:str = "", app_key: str = "") -> Optional[dict]:
res = self.db.table('node').where('api_key=?', (api_key, app_key)).find()
if isinstance(res, dict):
return res
else:
return None
def get_node_list(self,
search: str = "",
category_id: int = -1,
offset: int = 0,
limit: int = 10) -> Tuple[List[Dict], str]:
try:
args = []
query_str = ""
if search:
query_str += "remarks like ?"
args.append('%{}%'.format(search))
if category_id >= 0:
if query_str:
query_str += " and category_id=?"
else:
query_str += "category_id=?"
args.append(category_id)
if query_str:
data_list = self.db.table('node').where(query_str, args).order('id desc').limit(limit, offset).select()
else:
data_list = self.db.table('node').order('id desc').limit(limit, offset).select()
if self.db.ERR_INFO:
return [], self.db.ERR_INFO
if not isinstance(data_list, list):
return [], str(data_list)
return data_list, ""
except Exception as e:
return [], str(e)
def query_node_list(self, *args) -> List[Dict]:
return self.db.table('node').where(*args).select()
def category_exites(self, category_id: int) -> bool:
return self.db.table('category').where('id=?', (category_id,)).count() > 0
def node_id_exites(self, node_id: int) -> bool:
return self.db.table('node').where('id=?', (node_id,)).count() > 0
def category_map(self) -> Dict:
default_data = {0: "Default classification"}
data_list = self.db.table('category').field('id,name').select()
if isinstance(data_list, list):
for data in data_list:
default_data[data["id"]] = data["name"]
return default_data
def node_map(self) -> Dict:
default_data = {}
data_list = self.db.table('node').field('id,remarks').select()
if isinstance(data_list, list):
for data in data_list:
default_data[data["id"]] = data["remarks"]
return default_data
def create_category(self, name: str) -> str:
if self.db.table('category').where('name=?', (name,)).count() > 0:
return "The classification for this name already exists"
try:
res = self.db.table('category').insert({"name": name, "create_time": time.strftime('%Y-%m-%d %H:%M:%S')})
if isinstance(res, str):
return res
except Exception as e:
return str(e)
return ""
def delete_category(self, category_id: int):
self.db.table('node').where('category_id=?', (category_id,)).update({"category_id": 0})
self.db.table('category').where('id=?', (category_id,)).delete()
def bind_category_to_node(self, node_id: List[int], category_id: int) -> str:
if not node_id:
return "Node ID cannot be empty"
if category_id > 0 and not self.category_exites(category_id):
return "Classification does not exist"
try:
err = self.db.table('node').where(
'id in ({})'.format(",".join(["?"]*len(node_id))), (*node_id,)
).update({"category_id": category_id})
if isinstance(err, str):
return err
except Exception as e:
return str(e)
return ""
def node_count(self, search, category_id) -> int:
try:
args = []
query_str = ""
if search:
query_str += "remarks like ?"
args.append('%{}%'.format(search))
if category_id >= 0:
if query_str:
query_str += " and category_id=?"
else:
query_str += "category_id=?"
args.append(category_id)
if query_str:
count = self.db.table('node').where(query_str, args).order('id desc').count()
else:
count = self.db.table('node').order('id desc').count()
return count
except:
return 0
def get_node_by_id(self, node_id: int) -> Optional[Dict]:
try:
data = self.db.table('node').where('id=?', (node_id,)).find()
if self.db.ERR_INFO:
return None
if not isinstance(data, dict):
return None
return data
except:
return None
class ServerMonitorRepo:
_REPO_DIR = public.get_panel_path() + "/data/mod_node_status_cache/"
def __init__(self):
if not os.path.exists(self._REPO_DIR):
os.makedirs(self._REPO_DIR)
def set_wait_reboot(self, server_ip: str, start: bool):
wait_file = os.path.join(self._REPO_DIR, "wait_reboot_{}".format(server_ip))
if start:
return public.writeFile(wait_file, "wait_reboot")
else:
if os.path.exists(wait_file):
os.remove(wait_file)
def is_reboot_wait(self, server_ip: str):
wait_file = os.path.join(self._REPO_DIR, "wait_reboot_{}".format(server_ip))
# 重器待等待时间超过10分钟认为超时
return os.path.exists(wait_file) and os.path.getmtime(wait_file) > time.time() - 610
@staticmethod
def get_local_server_status():
from system import system
return system().GetNetWork(None)
def get_server_status(self, server_id: int) -> Optional[Dict]:
cache_file = os.path.join(self._REPO_DIR, "server_{}.json".format(server_id))
if not os.path.exists(cache_file):
return None
mtime = os.path.getmtime(cache_file)
if time.time() - mtime > 60 * 5:
os.remove(cache_file)
return None
try:
data = public.readFile(cache_file)
if isinstance(data, str):
return json.loads(data)
except:
return None
def save_server_status(self, server_id: int, data: Dict) -> str:
cache_file = os.path.join(self._REPO_DIR, "server_{}.json".format(server_id))
try:
public.writeFile(cache_file, json.dumps(data))
return ""
except Exception as e:
return str(e)
def remove_cache(self, server_id: int):
cache_file = os.path.join(self._REPO_DIR, "server_{}.json".format(server_id))
if os.path.exists(cache_file):
os.remove(cache_file)