462 lines
16 KiB
Python
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)
|