1961 lines
77 KiB
Python
1961 lines
77 KiB
Python
import hashlib
|
||
import html
|
||
import io
|
||
import ipaddress
|
||
import json
|
||
import os
|
||
import re
|
||
import shutil
|
||
import sys
|
||
import threading
|
||
import time
|
||
import socket
|
||
import platform
|
||
from uuid import uuid4
|
||
|
||
import math
|
||
import requests
|
||
import simple_websocket
|
||
import websocket
|
||
from werkzeug.datastructures import FileStorage
|
||
from datetime import datetime
|
||
|
||
if "/www/server/panel/class" not in sys.path:
|
||
sys.path.insert(0, "/www/server/panel/class")
|
||
|
||
import public
|
||
from typing import Optional, Tuple, List, Callable, Union, Any, Dict
|
||
from .one_panel_api import OnePanelApiClient
|
||
from mod.project.node.dbutil import Node, NodeAPPKey
|
||
|
||
try:
|
||
from YakPanel import cache
|
||
except:
|
||
class _Cache:
|
||
def __init__(self):
|
||
self._data = {}
|
||
|
||
def get(self, key):
|
||
return self._data.get(key, None)
|
||
|
||
def set(self, key, value, timeout):
|
||
self._data[key] = value
|
||
|
||
|
||
cache = _Cache()
|
||
|
||
|
||
def _get_os_info() -> Tuple[str, str]:
|
||
os_data = public.readFile("/etc/os-release")
|
||
if not os_data:
|
||
return "", ""
|
||
|
||
version = ["", ""]
|
||
for line in os_data.split("\n"):
|
||
if line.startswith("NAME="):
|
||
name = line.split("=")[1].replace('"', "").strip()
|
||
name_arr = name.split(" ")
|
||
if len(name_arr) > 1:
|
||
name = name_arr[0]
|
||
version[0] = name
|
||
elif line.startswith("VERSION="):
|
||
ver = line.split("=")[1].replace('"', "").strip()
|
||
ver_arr = ver.split(" ")
|
||
if len(ver_arr) > 1:
|
||
ver = ver_arr[0]
|
||
version[1] = ver
|
||
|
||
version = " ".join(version)
|
||
return version, platform.machine()
|
||
|
||
|
||
class ServerNode:
|
||
is_local = False
|
||
node_db_file = public.get_panel_path() + "/data/db/node.db"
|
||
|
||
def __init__(self, origin: str, api_key: str, app_key: str, remarks: str = "", timeout: int = 20):
|
||
self.origin = origin
|
||
self.api_key = api_key
|
||
self.app_key: Optional[NodeAPPKey] = Node.parse_app_key(app_key)
|
||
self.remarks = remarks
|
||
self.timeout = timeout
|
||
|
||
@property
|
||
def node_server_ip(self):
|
||
from urllib.parse import urlparse
|
||
host = urlparse(self.origin).hostname
|
||
try:
|
||
if isinstance(host, str) and ipaddress.ip_address(host):
|
||
return host
|
||
except:
|
||
pass
|
||
try:
|
||
ip_address = socket.gethostbyname(host)
|
||
return ip_address
|
||
except socket.gaierror as e:
|
||
print(f"Error: {e}")
|
||
return host
|
||
|
||
def app_bind(self) -> Optional[str]:
|
||
if self.app_key is None:
|
||
return "App key parsing failed, please confirm key information again"
|
||
version, arch = _get_os_info()
|
||
# public.print_log(self.app_key.app_token)
|
||
pdata = {
|
||
"bind_token": self.app_key.app_token,
|
||
"client_brand": "Yak-Panel",
|
||
"client_model": "Linux" if not version else "Linux {} {}".format(version, arch)
|
||
}
|
||
header = {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
"User-Agent": "Yak-Panel/Node Manager"
|
||
}
|
||
bind_url = self.app_key.origin + "/check_bind"
|
||
try:
|
||
res = requests.post(bind_url, data=pdata, headers=header, verify=False, timeout=self.timeout)
|
||
if res.status_code != 200:
|
||
return "Binding request failed, returned status code:%s,响应信息为:%s" % (res.status_code, res.text)
|
||
res_str = res.text.strip()
|
||
if res_str == "0":
|
||
return "Binding failed, please check if your key is correct"
|
||
elif res_str == "1":
|
||
return None
|
||
else:
|
||
return "Binding failed, error message is:{}".format(json.loads(res_str))
|
||
except Exception as e:
|
||
return "Network connection failed, unable to request to the target server"
|
||
|
||
def app_bind_status(self) -> Optional[str]:
|
||
if self.app_key is None:
|
||
return "App key parsing failed, please confirm key information again"
|
||
pdata = {"bind_token": self.app_key.app_token}
|
||
header = {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
"User-Agent": "Yak-Panel/Node Manager"
|
||
}
|
||
bind_url = self.app_key.origin + "/get_app_bind_status"
|
||
try:
|
||
resp = requests.post(bind_url, data=pdata, headers=header, verify=False, timeout=self.timeout)
|
||
if not resp.status_code == 200:
|
||
return "Error in obtaining binding status response status code. Please check if the node address and API are correct. The current status code is {},The return information is {}".format(
|
||
resp.status_code, resp.text)
|
||
res_str = resp.text.strip()
|
||
if res_str == "0":
|
||
return "Your device is not bound, please try to rebind it"
|
||
elif res_str == "1":
|
||
return None
|
||
else:
|
||
return "Request binding status failed with error message:{}".format(json.loads(res_str))
|
||
except Exception as e:
|
||
return "Failed to obtain binding status. Please check if the node app key is correct. The error message is:{}".format(str(e))
|
||
|
||
@classmethod
|
||
def new_by_id(cls, node_id: int) -> Optional[Union['ServerNode', 'LocalNode', 'LPanelNode']]:
|
||
data = public.S('node', cls.node_db_file).where("id = ?", (node_id,)).find()
|
||
if not data or not isinstance(data, dict):
|
||
return None
|
||
return cls.new_by_data(data)
|
||
|
||
@classmethod
|
||
def new_by_data(cls, node_data: dict):
|
||
if node_data["api_key"] == "local" and node_data["app_key"] == "local":
|
||
return LocalNode()
|
||
|
||
if node_data['lpver']:
|
||
lp = LPanelNode(
|
||
address=node_data['address'],
|
||
api_key=node_data['api_key'],
|
||
lpver=node_data['lpver'],
|
||
timeout=node_data.get('timeout', 20)
|
||
)
|
||
lp.remarks = node_data['remarks']
|
||
return lp
|
||
if not node_data["api_key"] and not node_data["app_key"]:
|
||
return None
|
||
|
||
return cls(
|
||
origin=node_data['address'],
|
||
api_key=node_data['api_key'],
|
||
app_key=node_data['app_key'],
|
||
remarks=node_data['remarks'],
|
||
timeout=node_data.get('timeout', 20)
|
||
)
|
||
|
||
def show_name(self) -> str:
|
||
if self.remarks:
|
||
return "{}({})".format(self.remarks, self.origin)
|
||
return self.origin
|
||
|
||
@staticmethod
|
||
def _get_node_ip(node_id: int) -> str:
|
||
data = public.S('node', public.get_panel_path() + "/data/db/node.db").where("id = ?", (node_id,)).find()
|
||
if not data or not isinstance(data, dict):
|
||
return ""
|
||
if data['address'].startswith("http"):
|
||
from urllib.parse import urlparse
|
||
host = urlparse(data['address']).hostname
|
||
else:
|
||
host = data['address']
|
||
try:
|
||
if isinstance(host, str) and ipaddress.ip_address(host):
|
||
return host
|
||
except:
|
||
pass
|
||
try:
|
||
ip_address = socket.gethostbyname(host)
|
||
return ip_address
|
||
except socket.gaierror as e:
|
||
print(f"Error: {e}")
|
||
return ""
|
||
|
||
@classmethod
|
||
def get_node_ip(cls, node_id: int) -> Optional[str]:
|
||
if public.S('node', cls.node_db_file).where("id=? AND app_key = 'local' AND api_key = 'local'", (node_id,)).count() > 0:
|
||
return "127.0.0.1"
|
||
cache_key = "mod_node_server_ip_{}".format(node_id)
|
||
c_ip = cache.get(cache_key)
|
||
if c_ip:
|
||
return c_ip
|
||
else:
|
||
ip = cls._get_node_ip(node_id)
|
||
if ip:
|
||
cache.set(cache_key, ip, 86400)
|
||
return ip
|
||
return None
|
||
|
||
@classmethod
|
||
def check_api_key(cls, node: Node) -> str:
|
||
if not LPanelNode.check_api_key(node): # 如果1panel检查可以连接上,则标记为1panel并直接返回
|
||
return ""
|
||
|
||
node = cls(node.address, node.api_key, node.app_key)
|
||
data, err = node._request("/system", "GetSystemTotal")
|
||
if err:
|
||
return err
|
||
if isinstance(data, dict) and "version" in data:
|
||
return ""
|
||
if isinstance(data, dict) and not data:return "Validation error:Please check if the node information is correct"
|
||
return "Validation error:%s" % str(data)
|
||
|
||
def test_conn(self) -> str:
|
||
data, err = self._request("/system", "GetSystemTotal")
|
||
if err:
|
||
return err
|
||
if isinstance(data, dict) and "version" in data:
|
||
return ""
|
||
return "Validation error:%s" % str(data)
|
||
|
||
def version(self) -> Optional[str]:
|
||
data, err = self._request("/system", "GetSystemTotal")
|
||
if err:
|
||
return None
|
||
if isinstance(data, dict) and "version" in data:
|
||
return data["version"]
|
||
return None
|
||
|
||
def get_net_work(self) -> Tuple[Optional[dict], str]:
|
||
data, err = self._request("/system", "GetNetWork")
|
||
if err:
|
||
return None, err
|
||
if isinstance(data, dict) and "cpu" in data and "mem" in data:
|
||
return data, ""
|
||
return None, "data in wrong format: %s" % str(data)
|
||
|
||
def get_tmp_token(self) -> Tuple[str, str]:
|
||
data, err = self._request("/config", "get_tmp_token")
|
||
if err:
|
||
return "", err
|
||
if isinstance(data, dict) and "status" in data:
|
||
if data["status"] and "msg" in data:
|
||
return data["msg"], ""
|
||
return "", data.get("msg", str(data))
|
||
if isinstance(data, dict) and "result" in data:
|
||
return data["result"], ""
|
||
return "", "data in wrong format: %s" % str(data)
|
||
|
||
def get_bt_params(self, other_data: dict = None) -> Dict:
|
||
now = int(time.time() * 1000)
|
||
other_data = other_data or {}
|
||
if self.app_key:
|
||
md5_panel_key = hashlib.md5(self.app_key.request_token.encode()).hexdigest()
|
||
request_token = hashlib.md5("{}{}".format(now, md5_panel_key).encode()).hexdigest()
|
||
other_data["request_token"] = request_token
|
||
other_data["request_time"] = str(now)
|
||
form_data = json.dumps(other_data)
|
||
return {
|
||
"client_bind_token": self.app_key.app_token,
|
||
"form_data": public.aes_encrypt(form_data, self.app_key.app_key)
|
||
}
|
||
else:
|
||
md5_panel_key = hashlib.md5(self.api_key.encode()).hexdigest()
|
||
request_token = hashlib.md5("{}{}".format(now, md5_panel_key).encode()).hexdigest()
|
||
other_data["request_token"] = request_token
|
||
other_data["request_time"] = str(now)
|
||
return other_data
|
||
|
||
def _request(self, path: str, action: str, pdata: dict = None) -> Tuple[Optional[any], str]:
|
||
url = "{}/v2{}".format(self.origin, path)
|
||
pdata = pdata or {}
|
||
bt_p = self.get_bt_params({"action": action, **pdata})
|
||
header = {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
"User-Agent": "Yak-Panel/Node Manager"
|
||
}
|
||
source_action=["DeleteDir","DeleteFile","CreateDir", "get_path_size"]
|
||
try:
|
||
resp = requests.post(url, data=bt_p, headers=header, verify=False, timeout=self.timeout)
|
||
if not resp.status_code == 200:
|
||
return None, "The response status code is incorrect. Please check if the node address and API are correct. The current response status code is {}".format(
|
||
resp.status_code)
|
||
if self.app_key:
|
||
real_data = public.aes_decrypt(resp.text, self.app_key.app_key)
|
||
if action in source_action:return json.loads(real_data), ""
|
||
return json.loads(real_data).get('message',{}), ""
|
||
if action in source_action:return resp.json(), ""
|
||
return resp.json().get('message',{}), ""
|
||
except Exception as e:
|
||
# public.print_error()
|
||
# return None, "请求节点失败,请检查节点地址和api是否正确,错误信息为:{}".format(str(e))
|
||
if self.remarks:
|
||
return None, "Request node [{}] failed, please check if the node address and API are correct".format(self.remarks)
|
||
return None, "Node request failed, please check if the node address and API are correct"
|
||
|
||
def get_file_body(self, path: str) -> Tuple[Optional[str], str]:
|
||
data, err = self._request("/files", "GetFileBody", pdata={"path": path})
|
||
if err:
|
||
return None, err
|
||
if not isinstance(data, dict):
|
||
if isinstance(data, dict) and "msg" in data:
|
||
return None, data["msg"]
|
||
return None, "data in wrong format"
|
||
return data["data"], ""
|
||
|
||
def php_site_list(self) -> Tuple[Optional[List[dict]], str]:
|
||
data, err = self._request("/mod/node/node/php_site_list", "", pdata={
|
||
"type": -1,
|
||
"p": 1,
|
||
"limit": 1000,
|
||
"table": "sites",
|
||
"search": "",
|
||
})
|
||
if err:
|
||
return None, err
|
||
|
||
if not isinstance(data, list):
|
||
if isinstance(data, dict) and "msg" in data:
|
||
return None, data["msg"]
|
||
return None, "data in wrong format"
|
||
|
||
return data, ""
|
||
|
||
def create_php_site(self, site_name: str, port: int, **kwargs) -> Tuple[Optional[int], str]:
|
||
path = "/www/wwwroot/{}".format(site_name)
|
||
if port not in (80, 443):
|
||
path = "/www/wwwroot/{}_{}".format(site_name, port)
|
||
webname = {
|
||
"domain": site_name if port in (80, 443) else "{}:{}".format(site_name, port),
|
||
"domainlist": [],
|
||
"count": 0
|
||
}
|
||
data, err = self._request("/site", "AddSite", pdata={
|
||
"path": path,
|
||
"ftp": "false",
|
||
"type": "PHP",
|
||
"type_id": "0",
|
||
"ps": "{}【Load balancing node】".format(site_name),
|
||
"port": str(port),
|
||
"version": "00",
|
||
"sql": "false",
|
||
"webname": json.dumps(webname)
|
||
})
|
||
|
||
if err:
|
||
return None, err
|
||
if not isinstance(data, dict):
|
||
return None, "data in wrong format"
|
||
|
||
if isinstance(data, dict) and "msg" in data and not data.get("status", True):
|
||
return None, data["msg"]
|
||
|
||
site_id = data.get("siteId", None)
|
||
if not isinstance(site_id, int):
|
||
return None, "Data parsing error"
|
||
return site_id, ""
|
||
|
||
def set_firewall_open(self, port: int, protocol: str = "tcp") -> Tuple[bool, str]:
|
||
data, err = self._request("/firewall/com/set_port_rule", "", pdata={
|
||
"protocol": "udp" if protocol == "udp" else "tcp",
|
||
"port": str(port),
|
||
"choose": "all",
|
||
"types": "accept",
|
||
"chain": "INPUT",
|
||
"operation": "add",
|
||
"strategy": "accept",
|
||
})
|
||
if err:
|
||
return False, err
|
||
|
||
if isinstance(data, dict) and "msg" in data:
|
||
msg = data["msg"]
|
||
if "Port {} already exists".format(port) in msg:
|
||
return True, ""
|
||
elif "Setup successful" in msg:
|
||
return True, ""
|
||
return False, "Setup failed"
|
||
|
||
def add_domain(self, site_id: int, site_name: str, domain: str, port: int) -> Tuple[bool, str]:
|
||
data, err = self._request("/site", "AddDomain", pdata={
|
||
"domain": "{}:{}".format(domain, port),
|
||
"webname": site_name,
|
||
"id": str(site_id)
|
||
})
|
||
if err:
|
||
return False, err
|
||
|
||
if isinstance(data, dict) and "domains" in data:
|
||
for d in data["domains"]:
|
||
if d["name"] == domain:
|
||
if "Already bound" in d["msg"]:
|
||
return True, ""
|
||
elif "Added successfully" in d["msg"]:
|
||
return True, ""
|
||
return False, "Add failed, the target machine returned information as:{}".format(
|
||
data.get("msg", str(data)) if isinstance(data, dict) else str(data))
|
||
|
||
def has_domain(self, site_id: int, domain: str) -> bool:
|
||
data, err = self._request("/data", "getData", pdata={
|
||
"table": "domain",
|
||
"list": "True",
|
||
"search": str(site_id)
|
||
})
|
||
if err:
|
||
return False
|
||
if not isinstance(data, list):
|
||
return False
|
||
for d in data:
|
||
if d["name"] == domain:
|
||
return True
|
||
return False
|
||
|
||
# mode -> cover: 覆盖,ignore: 跳过,rename:重命名
|
||
def upload_file(self, filename: str, target_path: str, mode: str = "cover",
|
||
call_log: Callable[[int, str], None] = None) -> str:
|
||
if not os.path.isfile(filename):
|
||
return "File: {} does not exist".format(filename)
|
||
|
||
target_file = os.path.join(target_path, os.path.basename(filename))
|
||
exits, err = self.target_file_exits(target_file)
|
||
if err:
|
||
return err
|
||
if exits and mode == "ignore":
|
||
call_log(0, "File upload:{} -> {},The target file already exists, skip uploading".format(filename, target_file))
|
||
return ""
|
||
if exits and mode == "rename":
|
||
upload_name = "{}_{}".format(os.path.basename(filename), public.md5(filename))
|
||
call_log(0, "File upload:{} -> {},The target file already exists, it will be renamed to {}".format(filename, target_file, upload_name))
|
||
else:
|
||
upload_name = os.path.basename(filename)
|
||
|
||
if os.path.getsize(filename) > 1024 * 1024 * 5:
|
||
return self._upload_big_file(filename, target_path, upload_name, call_log)
|
||
else:
|
||
return self._upload_little_file(filename, target_path, upload_name, call_log)
|
||
|
||
def target_file_exits(self, target_file: str) -> Tuple[bool, str]:
|
||
data, err = self._request("/files", "upload_files_exists", pdata={
|
||
"files": target_file,
|
||
})
|
||
if err:
|
||
return False, err
|
||
if not isinstance(data, list):
|
||
return False, "data in wrong format: %s" % str(data)
|
||
for f in data:
|
||
if f["filename"] == target_file and f["exists"]:
|
||
return True, ""
|
||
return False, ""
|
||
|
||
def upload_check(self, target_file_list: List[str]) -> Tuple[List[dict], str]:
|
||
data, err = self._request("/files", "upload_files_exists", pdata={
|
||
"files": "\n".join(target_file_list),
|
||
})
|
||
if err:
|
||
return [], err
|
||
if not isinstance(data, list):
|
||
if isinstance(data, dict):
|
||
return [], data.get("msg", "data in wrong format")
|
||
return [], "data in wrong format: %s" % str(data)
|
||
return data, ""
|
||
|
||
def _upload_big_file(self, filename: str, target_path: str, upload_name: str,
|
||
call_log: Callable[[int, str], None] = None) -> str:
|
||
url = "{}/files".format(self.origin)
|
||
bt_p = self.get_bt_params({"action": "upload"})
|
||
header = {"User-Agent": "Yak-Panel/Node Manager"}
|
||
try:
|
||
fb = open(filename, 'rb')
|
||
except Exception as e:
|
||
public.print_error()
|
||
return "File {} failed to open, please check file permissions, error message is:{}".format(filename, str(e))
|
||
|
||
file_size = os.path.getsize(filename)
|
||
for i in range(0, file_size, 1024 * 1024 * 5):
|
||
file_data = fb.read(1024 * 1024 * 5)
|
||
files = {'blob': ('blob', file_data, 'application/octet-stream')}
|
||
data = {
|
||
'f_path': target_path,
|
||
'f_name': upload_name,
|
||
'f_size': file_size,
|
||
'f_start': i,
|
||
}
|
||
data.update(bt_p)
|
||
try:
|
||
resp = requests.post(url, data=data, files=files, headers=header, verify=False, timeout=self.timeout)
|
||
if not resp.status_code == 200:
|
||
return "The response status code for uploading the file is incorrect. Please check if the node address and API are correct. The current status code is {}, and the return message is:{}".format(
|
||
resp.status_code, resp.text)
|
||
resp_text = resp.text
|
||
resp_json = {}
|
||
if self.app_key:
|
||
resp_text = public.aes_decrypt(resp_text, self.app_key.app_key).strip()
|
||
if not resp_text.isdecimal():
|
||
resp_json = json.loads(resp_text)
|
||
else:
|
||
resp_json = resp.json()
|
||
|
||
if resp_text.isdecimal():
|
||
up_p = int(resp_text)
|
||
if up_p != i + len(file_data):
|
||
return "Upload file failed, file block size is inconsistent"
|
||
else:
|
||
call_log(up_p * 100 // file_size, "File upload:{} -> {},The uploaded size is:{}".format(
|
||
filename, upload_name, public.to_size(up_p)))
|
||
elif "status" in resp_json:
|
||
if not resp_json["status"]:
|
||
return "Upload file failed with error message:{}".format(resp_json["msg"])
|
||
else:
|
||
call_log(100, "File upload:{} -> {},Upload successful".format(filename, upload_name))
|
||
return ""
|
||
else:
|
||
return "Upload file failed, response message is:{}".format(resp_text)
|
||
except Exception as e:
|
||
public.print_error()
|
||
return "Upload file: {} failed with error message:{}".format(filename, str(e))
|
||
return ""
|
||
|
||
def _upload_little_file(self, filename: str, target_path: str, upload_name: str,
|
||
call_log: Callable[[int, str], None] = None) -> str:
|
||
url = "{}/files".format(self.origin)
|
||
bt_p = self.get_bt_params({"action": "upload"})
|
||
header = {"User-Agent": "Yak-Panel/Node Manager"}
|
||
try:
|
||
with open(filename, 'rb') as f:
|
||
file_data = f.read()
|
||
except Exception as e:
|
||
public.print_error()
|
||
return "File {} failed to open, please check file permissions, error message is:{}".format(filename, str(e))
|
||
|
||
file_size = os.path.getsize(filename)
|
||
files = {'blob': ('blob', file_data, 'application/octet-stream')}
|
||
data = {
|
||
'f_path': target_path,
|
||
'f_name': upload_name,
|
||
'f_size': file_size,
|
||
'f_start': 0
|
||
}
|
||
data.update(bt_p)
|
||
try:
|
||
resp = requests.post(url, data=data, files=files, headers=header, verify=False, timeout=self.timeout)
|
||
if not resp.status_code == 200:
|
||
return "The response status code for uploading the file is incorrect. Please check if the node address and API are correct. The current status code is {}, and the return message is:{}".format(
|
||
resp.status_code, resp.text)
|
||
resp_json = {}
|
||
if self.app_key:
|
||
resp_text = public.aes_decrypt(resp.text, self.app_key.app_key).strip()
|
||
if not resp_text.isdecimal():
|
||
resp_json = json.loads(resp_text)
|
||
else:
|
||
resp_json = resp.json()
|
||
if not resp_json["status"]:
|
||
return "Upload file failed with error message:{}".format(resp_json["msg"])
|
||
return ""
|
||
except Exception as e:
|
||
public.print_error()
|
||
return "Upload file: {} failed with error message:{}".format(filename, str(e))
|
||
|
||
def upload_proxy(self):
|
||
try:
|
||
from YakPanel import request
|
||
f_name = request.form.get('f_name')
|
||
f_path = request.form.get('f_path')
|
||
f_size = request.form.get('f_size')
|
||
f_start = request.form.get('f_start')
|
||
blob_file: FileStorage = request.files.getlist('blob')[0]
|
||
url = "{}/files".format(self.origin)
|
||
bt_p = self.get_bt_params({"action": "upload"})
|
||
header = {"User-Agent": "Yak-Panel/Node Manager"}
|
||
files = {'blob': ('blob', blob_file.stream, 'application/octet-stream')}
|
||
data = {
|
||
'f_path': f_path,
|
||
'f_name': f_name,
|
||
'f_size': f_size,
|
||
'f_start': f_start
|
||
}
|
||
data.update(bt_p)
|
||
resp = requests.post(url, data=data, files=files, headers=header, verify=False, timeout=self.timeout)
|
||
if not resp.status_code == 200:
|
||
return "The response status code for uploading the file is incorrect. Please check if the node address and API are correct. The current status code is {}, and the return message is:{}".format(
|
||
resp.status_code, resp.text)
|
||
resp_text = resp.text
|
||
resp_json = {}
|
||
if self.app_key:
|
||
resp_text = public.aes_decrypt(resp_text, self.app_key.app_key).strip()
|
||
if not resp_text.isdecimal():
|
||
resp_json = json.loads(resp_text)
|
||
else:
|
||
resp_json = resp.json()
|
||
if resp_text.isdecimal():
|
||
return int(resp_text)
|
||
elif "status" in resp_json:
|
||
return resp_json
|
||
else:
|
||
return {"status": False, "msg": "Upload file failed, response message is:{}".format(resp_text)}
|
||
except Exception as e:
|
||
public.print_error()
|
||
return {"status": False, "msg": "Upload file failed with error message:{}".format(str(e))}
|
||
|
||
def remove_file(self, filename: str, is_dir: bool) -> dict:
|
||
action = "DeleteDir" if is_dir else "DeleteFile"
|
||
data, err = self._request("/files", action, pdata={
|
||
"path": filename,
|
||
})
|
||
if err:
|
||
return {"status": False, "msg": "Failed to delete file, error message is:{}".format(err)}
|
||
|
||
if isinstance(data, dict):
|
||
return data
|
||
return {"status": False, "msg": "Failed to delete file, response message is:{}".format(data)}
|
||
|
||
def create_dir(self, path: str) -> Tuple[Optional[Dict], str]:
|
||
data, err = self._request("/files", action="CreateDir", pdata={
|
||
"path": path,
|
||
})
|
||
if err:
|
||
return None, "Failed to create directory, error message is:{}".format(err)
|
||
if isinstance(data, dict):
|
||
return data, ""
|
||
return None, "Failed to create directory, response message is:{}".format(data)
|
||
|
||
def dir_walk(self, path: str) -> Tuple[List[dict], str]:
|
||
data, err = self._request("/mod/node/file_transfer/dir_walk", "", pdata={
|
||
"path": path,
|
||
})
|
||
if err:
|
||
return [], err
|
||
|
||
if not isinstance(data, list):
|
||
if isinstance(data, dict) and "msg" in data:
|
||
return [], data["msg"]
|
||
return [], "data in wrong format: %s" + str(data)
|
||
|
||
return data, ""
|
||
|
||
def filetransfer_version_check(self) -> str:
|
||
ver = self.version()
|
||
if not ver:
|
||
return "Node {} connection error".format(self.show_name())
|
||
|
||
try:
|
||
ver_list = [int(i) for i in ver.split(".")]
|
||
# if self.app_key: # todo: 版本号检测
|
||
# if ver_list[0] > 11 or (ver_list[0] == 11 and ver_list[1] >= 1):
|
||
# return "Node {} version is lower than [11.1.0], unable to use app link, please upgrade node version".format(self.show_name())
|
||
|
||
if ver_list[0] >=8:
|
||
return ""
|
||
elif ver_list[0] == 7 and ver_list[1] >= 65:
|
||
return ""
|
||
elif ver_list[0] == 7 and ver_list[1] == 0 and ver_list[2] >= 30:
|
||
return ""
|
||
elif ver_list[0] ==2 and ver_list[1] >= 15:
|
||
return ""
|
||
return "Please upgrade the panel version to 【8.0.0/7.65.0/7.0.30/2.15.0】 or above before using it"
|
||
except:
|
||
pass
|
||
return "Please upgrade the panel version to 【8.0.0/7.65.0/7.0.30/2.15.0】 or above before using it"
|
||
|
||
|
||
def node_create_filetransfer_task(self, source_node: dict,
|
||
target_node: dict,
|
||
source_path_list: List[dict],
|
||
target_path: str,
|
||
created_by: str,
|
||
default_mode: str = "cover") -> dict:
|
||
data, err = self._request("/mod/node/file_transfer/node_create_filetransfer_task", "", pdata={
|
||
"source_node": json.dumps(source_node),
|
||
"target_node": json.dumps(target_node),
|
||
"source_path_list": json.dumps(source_path_list),
|
||
"target_path": target_path,
|
||
"created_by": created_by,
|
||
"default_mode": default_mode,
|
||
})
|
||
if err:
|
||
return {"status": False, "msg": "Failed to create file transfer task, error message is:{}".format(err)}
|
||
if isinstance(data, dict):
|
||
return data
|
||
else:
|
||
return {"status": False, "msg": "Failed to create file transfer task, response message is:{}".format(data)}
|
||
|
||
def file_list(self, path: str, p: int, row: int, search: str) -> Tuple[Dict, str]:
|
||
data, err = self._request("/files", "GetDirNew", pdata={
|
||
"path": path,
|
||
"p": p,
|
||
"showRow": row,
|
||
"search": search,
|
||
})
|
||
if err:
|
||
return {}, err
|
||
if not isinstance(data, dict):
|
||
return {}, "data in wrong format: %s" + str(data)
|
||
return data, ""
|
||
|
||
def get_transfer_status(self, task_id: int) -> dict:
|
||
data, err = self._request("/mod/node/file_transfer/get_transfer_status", "", pdata={
|
||
"task_id": task_id,
|
||
})
|
||
if err:
|
||
return {"status": False, "msg": "Failed to retrieve the status of the file transfer task, error message is:{}".format(err)}
|
||
if isinstance(data, dict):
|
||
return data
|
||
return {"status": False, "msg": "Failed to obtain file transfer task status, response message is:{}".format(data)}
|
||
|
||
def proxy_transfer_status(self, task_id: int, ws: simple_websocket.Server):
|
||
err = self._proxy_websocket(
|
||
call_data={
|
||
"mod_name": "node",
|
||
"sub_mod_name": "file_transfer",
|
||
"def_name": "node_proxy_transfer_status",
|
||
"callback": "node_proxy_transfer_status",
|
||
"data": {
|
||
"task_id": task_id,
|
||
}
|
||
},
|
||
uri="ws_modsoc",
|
||
call_back=ws.send
|
||
)
|
||
if err:
|
||
ws.send(json.dumps({
|
||
"type": "error",
|
||
"msg": "Failed to retrieve the status of the file transfer task, error message is:{}".format(err)
|
||
}))
|
||
|
||
def _proxy_websocket(self, call_data: dict, uri: str, call_back: Callable[[Any],None]) -> str:
|
||
from urllib.parse import urlencode, urlparse
|
||
from ssl import CERT_NONE
|
||
try:
|
||
bt_p = self.get_bt_params()
|
||
header = {"User-Agent": "Yak-Panel/Node Manager"}
|
||
u = urlparse(self.origin)
|
||
url = (
|
||
"{}://{}/{}?{}".format(
|
||
"ws" if u.scheme == "http" else "wss",
|
||
u.netloc, uri, urlencode(bt_p)
|
||
)
|
||
)
|
||
ws_req = websocket.WebSocket(sslopt={"cert_reqs": CERT_NONE}) # 忽略证书
|
||
ws_req.connect(url, header=header)
|
||
ws_req.send("{}") # 跳过x-http-tokn 验证
|
||
ws_req.send(json.dumps(call_data)) # 发送调用数据
|
||
|
||
while True:
|
||
data = ws_req.recv()
|
||
if data == "{}":
|
||
break
|
||
if data:
|
||
call_back(data)
|
||
else:
|
||
break
|
||
ws_req.close()
|
||
return ""
|
||
except Exception as e:
|
||
return str(e)
|
||
|
||
def proxy_transferfile_status(self, task_id: int, exclude_nodes: List[int], the_log_id: int, call_back: Callable[[Any],None]) -> str:
|
||
return self._proxy_websocket(
|
||
call_data={
|
||
"mod_name": "node",
|
||
"sub_mod_name": "executor",
|
||
"def_name": "node_proxy_transferfile_status",
|
||
"callback": "node_proxy_transferfile_status",
|
||
"data": {
|
||
"task_id": task_id,
|
||
"exclude_nodes": exclude_nodes,
|
||
"the_log_id": the_log_id
|
||
}
|
||
},
|
||
uri="ws_modsoc",
|
||
call_back=call_back
|
||
)
|
||
|
||
def node_create_transfer_task(self, transfer_task_data:dict) -> dict:
|
||
data, err = self._request(
|
||
"/mod/node/executor/node_create_transfer_task", "", pdata={
|
||
"transfer_task_data": json.dumps(transfer_task_data),
|
||
}
|
||
)
|
||
if err:
|
||
return {"status": False, "msg": "Failed to create file transfer task, error message is:{}".format(err)}
|
||
if isinstance(data, dict):
|
||
return data
|
||
else:
|
||
return {"status": False, "msg": "Failed to create file transfer task, response message is:{}".format(data)}
|
||
|
||
def node_transferfile_status_history(self, transfer_task_id: int, only_error=True):
|
||
data, err = self._request(
|
||
"/mod/node/executor/node_transferfile_status_history", "", pdata={
|
||
"task_id": transfer_task_id,
|
||
"only_error": 1 if only_error else 0
|
||
}
|
||
)
|
||
if err:
|
||
return {"status": False, "msg": "Failed to create file transfer task, error message is:{}".format(err)}
|
||
if isinstance(data, dict):
|
||
return data
|
||
else:
|
||
return {"status": False, "msg": "Failed to create file transfer task, response message is:{}".format(data)}
|
||
|
||
def download_proxy(self, filename: str):
|
||
url = "{}/download".format(self.origin)
|
||
bt_p = self.get_bt_params()
|
||
header = {"User-Agent": "Yak-Panel/Node Manager"}
|
||
try:
|
||
resp = requests.get(url, params={"filename": filename}, data=bt_p, headers=header, stream=True, verify=False, timeout=self.timeout)
|
||
if not resp.status_code == 200:
|
||
return "The response status code for downloading the file is incorrect. Please check if the node address and API are correct. The current status code is {}, and the return message is:{}".format(
|
||
resp.status_code, resp.text)
|
||
|
||
from flask import send_file, stream_with_context, Response
|
||
filename = os.path.basename(filename)
|
||
if resp.headers.get("Content-Disposition", "").find("filename=") != -1:
|
||
filename = resp.headers.get("Content-Disposition", "").split("filename=")[1]
|
||
|
||
def generate():
|
||
for chunk in resp.iter_content(chunk_size=1024 * 1024 * 5):
|
||
if chunk:
|
||
yield chunk
|
||
|
||
# 设置响应头
|
||
headers = {
|
||
'Content-Type': resp.headers.get('Content-Type', 'application/octet-stream'),
|
||
'Content-Disposition': 'attachment; filename="{}"'.format(filename),
|
||
'Content-Length': resp.headers.get('Content-Length', ''),
|
||
'Accept-Ranges': 'bytes'
|
||
}
|
||
|
||
# 使用 stream_with_context 确保请求上下文在生成器运行时保持活跃
|
||
return Response(
|
||
stream_with_context(generate()),
|
||
headers=headers,
|
||
direct_passthrough=True
|
||
)
|
||
except Exception as e:
|
||
public.print_error()
|
||
return "Download file: {} failed with error message:{}".format(filename, str(e))
|
||
|
||
def download_file(self, filename: str, target_path: str, mode: str,
|
||
call_log: Callable[[int, str], None] = None) -> str:
|
||
target_file = os.path.join(target_path, os.path.basename(filename))
|
||
exits = os.path.exists(target_file)
|
||
if exits and mode == "ignore":
|
||
call_log(0, "File Download:{} -> {},The target file already exists, skip download".format(filename, target_file))
|
||
return ""
|
||
if exits and mode == "rename":
|
||
download_name = "{}_{}".format(os.path.basename(filename), public.md5(filename))
|
||
call_log(0, "File Download:{} -> {},The target file already exists, it will be renamed to {}".format(filename, target_file, download_name))
|
||
else:
|
||
download_name = os.path.basename(filename)
|
||
|
||
return self._download_file(filename, target_path, download_name, call_log)
|
||
|
||
def _download_file(self, filename: str, target_path: str, download_name: str,
|
||
call_log: Callable[[int, str], None] = None) -> str:
|
||
data, err = self.upload_check([filename])
|
||
if err:
|
||
return "Request file: {} status failed with error message:{}".format(filename, err)
|
||
file_size: Optional[int] = None
|
||
for i in data:
|
||
if i["filename"] == filename and i["isfile"] == True:
|
||
file_size = i["size"]
|
||
break
|
||
|
||
if file_size is None:
|
||
return "File {} does not exist, skip download".format(filename)
|
||
try:
|
||
if not os.path.isdir(target_path):
|
||
os.makedirs(target_path)
|
||
except Exception as e:
|
||
return "Failed to create folder {}, please check folder permissions, error message is:{}".format(target_path, str(e))
|
||
|
||
if file_size == 0:
|
||
fp = open(os.path.join(target_path, download_name), "w")
|
||
fp.close()
|
||
return ""
|
||
|
||
tmp_file = os.path.join(target_path, "{}.{}".format(download_name, uuid4().hex))
|
||
try:
|
||
if not os.path.exists(target_path):
|
||
os.makedirs(target_path)
|
||
fb = open(tmp_file, 'wb')
|
||
except Exception as e:
|
||
return "Failed to create temporary file {}, please check folder permissions, error message is:{}".format(tmp_file, str(e))
|
||
|
||
url = "{}/download".format(self.origin)
|
||
bt_p = self.get_bt_params()
|
||
header = {"User-Agent": "Yak-Panel/Node Manager"}
|
||
try:
|
||
resp = requests.get(url, params={"filename": filename}, data=bt_p, headers=header, stream=True, verify=False, timeout=self.timeout)
|
||
if not resp.status_code == 200:
|
||
return "The response status code for downloading the file is incorrect. Please check if the node address and API are correct. The current status code is {}, and the return message is:{}".format(
|
||
resp.status_code, resp.text)
|
||
|
||
now_size = 0
|
||
for chunk in resp.iter_content(chunk_size=1024 * 1024 * 3):
|
||
if chunk:
|
||
now_size += len(chunk)
|
||
fb.write(chunk)
|
||
fb.flush()
|
||
call_log(now_size * 100 // file_size,
|
||
"Download file {}, Downloaded: {}".format(filename, public.to_size(now_size)))
|
||
if fb.tell() != file_size:
|
||
return "Download file {} failed, file size check error".format(filename)
|
||
fb.close()
|
||
shutil.move(tmp_file, os.path.join(target_path, download_name))
|
||
return ""
|
||
except Exception as e:
|
||
return "Download file {} failed,The error message is:{}".format(filename, str(e))
|
||
finally:
|
||
if not fb.closed:
|
||
fb.close()
|
||
if os.path.exists(tmp_file):
|
||
os.remove(tmp_file)
|
||
|
||
def dir_size(self, path: str) -> Tuple[Optional[int], str]:
|
||
data, err = self._request("/files", "get_path_size", pdata={
|
||
"path": path,
|
||
})
|
||
if err:
|
||
return None, err
|
||
if isinstance(data, dict) and "size" in data:
|
||
return data["size"], ""
|
||
return None, "Failed to retrieve directory size, response message is:{}".format(data)
|
||
|
||
def get_sshd_port(self) -> Optional[int]:
|
||
data, err = self._request("/safe/ssh/GetSshInfo", "", pdata={})
|
||
if err:
|
||
return None
|
||
if isinstance(data, dict) and "port" in data:
|
||
return int(data["port"])
|
||
return None
|
||
|
||
def restart_bt_panel(self) -> Dict[str, Any]:
|
||
res, err = self._request("/system", "ReWeb", pdata={})
|
||
if err:
|
||
return {"status": False, "msg": err}
|
||
return {"status": True, "msg": "Restart successful"}
|
||
|
||
def server_reboot(self):
|
||
res, err = self._request("/system", "ServiceAdmin", pdata={
|
||
"name": "nginx",
|
||
"type": "stop",
|
||
})
|
||
if err:
|
||
return {"status": False, "msg": err}
|
||
if res.get('result','')!='Executed successfully!':
|
||
return {"status": False, "msg": "Nginx failed to stop, unable to continue executing server restart:" + res["msg"]}
|
||
|
||
res, err = self._request("/system", "ServiceAdmin", pdata={
|
||
"name": "mysqld",
|
||
"type": "stop",
|
||
})
|
||
if err:
|
||
return {"status": False, "msg": err}
|
||
if res.get('result','')!='Executed successfully!':
|
||
return {"status": False, "msg": "MySQL service failed to stop, unable to continue executing server restart:" + res["msg"]}
|
||
|
||
res, _ = self._request("/system", "RestartServer", pdata={})
|
||
if res.get('result','')!='Command sent successfully!':
|
||
return {"status": False, "msg": "Server restart failed:" + res["msg"]}
|
||
|
||
from mod.project.node.dbutil import ServerMonitorRepo
|
||
repo = ServerMonitorRepo()
|
||
repo.set_wait_reboot(self.node_server_ip, True)
|
||
|
||
def wait_for_reboot():
|
||
# wait 等待服务器重启成功, 超时时间默认为 10 分钟
|
||
wait_for = time.time() + 600
|
||
time.sleep(3)
|
||
while time.time() < wait_for:
|
||
if self.test_conn() == "": # 无错误时表示重启成功
|
||
repo.set_wait_reboot(self.node_server_ip, False)
|
||
from mod.project.node.dbutil import ServerNodeDB
|
||
if self.app_key !=None:self.app_key=self.app_key.to_string()
|
||
node_data = ServerNodeDB().find_node(api_key=self.api_key, app_key=self.app_key)
|
||
if node_data:
|
||
monitor_node_once(node_data)
|
||
return {"status": True, "msg": "Server restart successful"}
|
||
time.sleep(3)
|
||
# public.print_log("Waiting for server restart... {}".format(wait_for - time.time()))
|
||
repo.set_wait_reboot(self.node_server_ip, False)
|
||
return {"status": False, "msg": "Restarting server failed, server information has not been detected for more than 10 minutes"}
|
||
|
||
t = threading.Thread(target=wait_for_reboot, daemon=True)
|
||
t.start()
|
||
|
||
return {"status": True, "msg": "The server restart has started, please wait patiently for the successful restart"}
|
||
|
||
def read_ssh_key(self) -> Tuple[Optional[str], str]:
|
||
data, err = self._request("/ssh_security", "get_key", pdata={})
|
||
if err:
|
||
return None, err
|
||
if not isinstance(data, dict) or "status" not in data:
|
||
return None, "Failed to obtain SSH key, please check if the node address and API are correct"
|
||
if not data["status"]:
|
||
return None, data["msg"]
|
||
return data["msg"], ""
|
||
|
||
def get_dir(self, path: str, search: str, disk: str):
|
||
data, err = self._request("/files", "GetDir", pdata={
|
||
"path": path, "disk": disk, "search": search
|
||
})
|
||
if err:
|
||
return {"status": False, "msg": err}
|
||
if not isinstance(data, dict):
|
||
return {"status": False, "msg": "Failed to retrieve directory, please check if the node address and API are correct"}
|
||
return data
|
||
|
||
def upload_dir_check(self, target_file: str) -> str:
|
||
data, err = self._request("/files", "upload_files_exists", pdata={"files": target_file})
|
||
if err:
|
||
return err
|
||
if not isinstance(data, list):
|
||
return "data in wrong format: %s" % str(data)
|
||
for f in data:
|
||
if f["filename"] == target_file and f["exists"]:
|
||
if f["isfile"]:
|
||
return "The name path is not a directory"
|
||
else:
|
||
return ""
|
||
return ""
|
||
|
||
|
||
def monitor_all_node_status():
|
||
all_nodes = public.S('node', public.get_panel_path() + "/data/db/node.db").select()
|
||
if not isinstance(all_nodes, list) or not all_nodes:
|
||
return
|
||
|
||
import threading
|
||
|
||
threads = []
|
||
for tmp_node_data in all_nodes:
|
||
t = threading.Thread(target=monitor_node_once, args=(tmp_node_data,))
|
||
t.start()
|
||
threads.append(t)
|
||
|
||
# 等待所有线程完成(可选)
|
||
for t in threads:
|
||
t.join()
|
||
|
||
|
||
def monitor_node_once(node_data: dict):
|
||
from mod.project.node.dbutil import Node, ServerNodeDB, ServerMonitorRepo
|
||
from mod.project.node.nodeutil.ssh_wrap import SSHApi
|
||
from mod.base.ssh_executor import test_ssh_config
|
||
try:
|
||
if node_data["app_key"] == "local" and node_data["api_key"] == "local":
|
||
return
|
||
node_data["error"] = json.loads(node_data["error"])
|
||
node_data["ssh_conf"] = json.loads(node_data["ssh_conf"])
|
||
node, err = Node.from_dict(node_data)
|
||
if err:
|
||
# public.print_log("Node data parsing error:{}".format(err))
|
||
return
|
||
|
||
if node_data["lpver"]:
|
||
srv = LPanelNode(node.address, node.api_key, node.lpver)
|
||
elif node.api_key or node.app_key:
|
||
srv = ServerNode(node.address, node.api_key, node.app_key)
|
||
else:
|
||
srv =SSHApi(**node_data["ssh_conf"])
|
||
|
||
data, err = srv.get_net_work()
|
||
if err:
|
||
node.error_num += 1
|
||
node.error = {
|
||
"msg": err,
|
||
"time": int(time.time())
|
||
}
|
||
if node.error_num >= 2:
|
||
node.status = 0
|
||
ServerMonitorRepo().remove_cache(node.id)
|
||
|
||
ServerNodeDB().update_node(node)
|
||
|
||
else:
|
||
if node.error_num > 0:
|
||
node.error_num = 0
|
||
node.error = {}
|
||
node.status = 1
|
||
ServerNodeDB().update_node(node)
|
||
|
||
if data:
|
||
repo = ServerMonitorRepo()
|
||
repo.save_server_status(node.id, data)
|
||
if repo.is_reboot_wait(node.server_ip):
|
||
repo.set_wait_reboot(node.server_ip, False)
|
||
if not node_data["ssh_test"] and not node_data["ssh_conf"] and not isinstance(srv, SSHApi):
|
||
ssh_key, err = srv.read_ssh_key()
|
||
if err:
|
||
# public.print_log("Failed to obtain SSH key:{}".format(err))
|
||
return
|
||
port = srv.get_sshd_port()
|
||
if not port:
|
||
# public.print_log("Failed to obtain SSH service port")
|
||
return
|
||
|
||
conf = {
|
||
"host": srv.node_server_ip,
|
||
"pkey": ssh_key,
|
||
"port": port,
|
||
"username": "root",
|
||
"password": "",
|
||
"pkey_passwd": "",
|
||
}
|
||
err = test_ssh_config(**conf)
|
||
if err:
|
||
# public.print_log("SSH key test failed:{}".format(err))
|
||
ServerNodeDB().set_node_ssh_conf(node.id, {}, ssh_test=1)
|
||
return
|
||
else:
|
||
ServerNodeDB().set_node_ssh_conf(node.id, conf, ssh_test=1)
|
||
|
||
except Exception as e:
|
||
if public.is_debug():
|
||
public.print_error()
|
||
|
||
|
||
def monitor_node_once_with_timeout(node_data: dict, timeout: int = 5):
|
||
if node_data["app_key"] == "local" and node_data["api_key"] == "local":
|
||
return
|
||
from mod.project.node.dbutil import Node, ServerNodeDB, ServerMonitorRepo
|
||
from mod.project.node.nodeutil.ssh_wrap import SSHApi
|
||
try:
|
||
node_data["error"] = json.loads(node_data["error"])
|
||
node_data["ssh_conf"] = json.loads(node_data["ssh_conf"])
|
||
node, err = Node.from_dict(node_data)
|
||
if err:
|
||
# public.print_log("Node data parsing error:{}".format(err))
|
||
return
|
||
|
||
if node_data["app_key"] or node_data["api_key"]:
|
||
node_data["timeout"] = timeout
|
||
srv = ServerNode.new_by_data(node_data)
|
||
node_data.pop("timeout")
|
||
elif node_data["ssh_conf"]:
|
||
srv = SSHApi(**node_data["ssh_conf"], timeout=timeout)
|
||
else:
|
||
return
|
||
|
||
data, err = srv.get_net_work()
|
||
if err:
|
||
node.error_num += 1
|
||
node.error = {
|
||
"msg": err,
|
||
"time": int(time.time())
|
||
}
|
||
if node.error_num >= 2:
|
||
node.status = 0
|
||
ServerMonitorRepo().remove_cache(node.id)
|
||
|
||
ServerNodeDB().update_node(node)
|
||
|
||
elif node.error_num > 0:
|
||
node.error_num = 0
|
||
node.error = {}
|
||
node.status = 1
|
||
ServerNodeDB().update_node(node)
|
||
|
||
if data:
|
||
repo = ServerMonitorRepo()
|
||
repo.save_server_status(node.id, data)
|
||
if repo.is_reboot_wait(node.server_ip):
|
||
repo.set_wait_reboot(node.server_ip, False)
|
||
except Exception as e:
|
||
if public.is_debug():
|
||
public.print_error()
|
||
|
||
|
||
class LocalNode(ServerNode):
|
||
is_local = True
|
||
|
||
def __init__(self):
|
||
super().__init__("", "", "")
|
||
|
||
def create_php_site(self, site_name: str, port: int = 80, **kwargs) -> Tuple[Optional[int], str]:
|
||
path = "/www/wwwroot/{}".format(site_name)
|
||
webname = {
|
||
"domain": site_name if port in (80, 443) else "{}:{}".format(site_name, port),
|
||
"domainlist": [],
|
||
"count": 0
|
||
}
|
||
pdata = {
|
||
"path": path,
|
||
"ftp": "false",
|
||
"type": "PHP",
|
||
"type_id": "0",
|
||
"ps": kwargs.get("ps") if kwargs.get("ps", None) else "{}【Load balancing site】".format(site_name),
|
||
"port": str(port),
|
||
"version": "00",
|
||
"sql": "false",
|
||
"webname": json.dumps(webname)
|
||
}
|
||
|
||
if "/www/server/panel/class" not in sys.path:
|
||
sys.path.insert(0, "/www/server/panel/class")
|
||
|
||
from panelSite import panelSite
|
||
|
||
res = panelSite().AddSite(public.to_dict_obj(pdata))
|
||
|
||
if "siteId" in res:
|
||
return res["siteId"], ""
|
||
if "status" in res and not res["status"]:
|
||
return 0, res.get("msg", "Unknown error")
|
||
return 0, "Unknown error"
|
||
|
||
@staticmethod
|
||
def site_proxy_list(site_name: str) -> List[dict]:
|
||
from mod.base.web_conf.proxy import RealProxy
|
||
data = RealProxy(config_prefix="").get_proxy_list(public.to_dict_obj({
|
||
"sitename": site_name
|
||
}))
|
||
if isinstance(data, list) and data:
|
||
return data
|
||
|
||
from panelSite import panelSite
|
||
|
||
data = panelSite().GetProxyList(public.to_dict_obj({
|
||
"sitename": site_name
|
||
}))
|
||
if isinstance(data, list) and data:
|
||
return data
|
||
return []
|
||
|
||
def show_name(self) -> str:
|
||
return "Local node"
|
||
|
||
def php_site_list(self) -> Tuple[List[dict], str]:
|
||
from mod.base.web_conf.ssl import RealSSLManger
|
||
ssl_m = RealSSLManger()
|
||
|
||
all_sites = public.M("sites").where("project_type=? and status = 1", "PHP").select()
|
||
res = []
|
||
for i in all_sites:
|
||
domain = public.M("domain").where("pid=?", i["id"]).select()
|
||
domains = list(set([x["name"] for x in domain]))
|
||
port = list(set([int(x["port"]) for x in domain]))
|
||
ssl = ssl_m.get_site_ssl_info(i["name"]) is not None
|
||
if ssl and 443 not in port:
|
||
port.append(443)
|
||
res.append({
|
||
"site_id": i["id"],
|
||
"site_name": i["name"],
|
||
"ports": port,
|
||
"domains": domains,
|
||
"ssl": ssl
|
||
})
|
||
|
||
return res, ""
|
||
|
||
def add_domain(self, site_id: int, site_name: str, domain: str, port: int) -> Tuple[bool, str]:
|
||
from panelSite import panelSite
|
||
res = panelSite().AddDomain(public.to_dict_obj({
|
||
"domain": "{}:{}".format(domain, port),
|
||
"webname": site_name,
|
||
"id": str(site_id)
|
||
}))
|
||
if isinstance(res, dict) and res.get("status", False):
|
||
return True, ""
|
||
return False, res.get("msg", "Unknown error")
|
||
|
||
def has_domain(self, site_id: int, domain: str):
|
||
return public.M("domain").where("pid=? and name=?", site_id, domain).count() > 0
|
||
|
||
def dir_walk(self, path: str) -> Tuple[List[dict], str]:
|
||
if not os.path.isdir(path):
|
||
return [], "{} Not a directory".format(path)
|
||
res_file = []
|
||
count = 0
|
||
empty_dir = []
|
||
for root, dirs, files in os.walk(path):
|
||
if not files:
|
||
empty_dir.append(root)
|
||
for f in files:
|
||
if count > 1000:
|
||
return [], "The number of directory files exceeds 1000, please compress before operating"
|
||
count += 1
|
||
try:
|
||
res_file.append({
|
||
"path": os.path.join(root, f),
|
||
"size": os.path.getsize(os.path.join(root, f)),
|
||
"is_dir": 0
|
||
})
|
||
except:
|
||
pass
|
||
return [{"path": d, "size": 0, "is_dir": 1} for d in empty_dir] + res_file, ""
|
||
|
||
def remove_file(self, filename: str, is_dir: bool) -> dict:
|
||
from files_v2 import files
|
||
if is_dir:
|
||
return files().DeleteDir(public.to_dict_obj({"path": filename}))
|
||
else:
|
||
return files().DeleteFile(public.to_dict_obj({"path": filename}))
|
||
|
||
def file_list(self, path: str, p: int, row: int, search: str) -> Tuple[Dict, str]:
|
||
from files_v2 import files
|
||
return files().GetDirNew(public.to_dict_obj({
|
||
"path": path,
|
||
"p": p,
|
||
"showRow": row,
|
||
"search": search
|
||
})), ""
|
||
|
||
def upload_proxy(self):
|
||
from files_v2 import files
|
||
return files().upload(args=public.to_dict_obj({}))
|
||
|
||
def upload_check(self, target_file_list: List[str]) -> Tuple[List[dict], str]:
|
||
from files_v2 import files
|
||
return files().upload_files_exists(args=public.to_dict_obj({
|
||
"files": "\n".join(target_file_list),
|
||
})), ""
|
||
|
||
def dir_size(self, path: str) -> Tuple[Optional[int], str]:
|
||
return public.get_path_size(path), ""
|
||
|
||
def get_sshd_port(self) -> Optional[int]:
|
||
return public.get_sshd_port()
|
||
|
||
def create_dir(self, path: str) -> Tuple[Optional[Dict], str]:
|
||
from files_v2 import files
|
||
return files().CreateDir(public.to_dict_obj({"path": path})), ""
|
||
|
||
def get_file_body(self, path: str) -> Tuple[Optional[str], str]:
|
||
res = public.readFile(path)
|
||
if isinstance(res, str):
|
||
return res, ""
|
||
return None, "fail to read file"
|
||
|
||
def read_ssh_key(self) -> Tuple[Optional[str], str]:
|
||
from ssh_security import ssh_security
|
||
data = ssh_security().get_key(None)
|
||
if data["status"]:
|
||
return data["msg"], ""
|
||
return None, data["msg"]
|
||
|
||
def get_dir(self, path: str, search: str, disk: str):
|
||
from files_v2 import files
|
||
return files().GetDir(public.to_dict_obj({"path": path, "search":search, "disk": disk}))
|
||
|
||
|
||
class LPanelNode(ServerNode):
|
||
_TIME_REGEXP = re.compile(r":\d{2}(?P<err>\.\d{6,})[+-]\d{2}:\d{2}")
|
||
|
||
def __init__(self, address: str, api_key: str, lpver: str = "v2", timeout: int = 20):
|
||
super().__init__(address, api_key, "", timeout=timeout)
|
||
self.ver = lpver
|
||
self.lpc = OnePanelApiClient(address, api_key, lpver, self.timeout)
|
||
|
||
@classmethod
|
||
def check_api_key(cls, node: Node) -> str:
|
||
# public.print_log(node.address, node.api_key)
|
||
lpc = OnePanelApiClient(node.address, node.api_key)
|
||
if lpc.test_ver():
|
||
node.lpver = lpc.ver
|
||
return ""
|
||
return "1 Panel node test connection failed"
|
||
|
||
def test_conn(self) -> str:
|
||
if self.lpc.test_ver():
|
||
return ""
|
||
return "1 Panel node test connection failed"
|
||
|
||
def get_net_work(self) -> Tuple[Optional[dict], str]:
|
||
"""{
|
||
"code": 200,
|
||
"message": "",
|
||
"data": {
|
||
"path": "/",
|
||
"name": "/",
|
||
"user": "root",
|
||
"group": "root",
|
||
"uid": "0",
|
||
"gid": "0",
|
||
"extension": "",
|
||
"content": "",
|
||
"size": 4096,
|
||
"isDir": true,
|
||
"isSymlink": false,
|
||
"isHidden": false,
|
||
"linkPath": "",
|
||
"type": "",
|
||
"mode": "0555",
|
||
"mimeType": "",
|
||
"updateTime": "0001-01-01T00:00:00Z",
|
||
"modTime": "2023-12-16T18:21:10.369198018+08:00",
|
||
"items": [],
|
||
"itemTotal": 22,
|
||
"favoriteID": 0,
|
||
"isDetail": true
|
||
}
|
||
}
|
||
API Response Status: 200
|
||
{
|
||
"code": 200,
|
||
"message": "",
|
||
"data": {
|
||
"uptime": 8060394,
|
||
"timeSinceUptime": "2025-03-05 10:02:04",
|
||
"procs": 173,
|
||
"load1": 1.05,
|
||
"load5": 1.05,
|
||
"load15": 1.05,
|
||
"loadUsagePercent": 17.5,
|
||
"cpuPercent": [
|
||
100,
|
||
0,
|
||
0,
|
||
0
|
||
],
|
||
"cpuUsedPercent": 25.000000232830644,
|
||
"cpuUsed": 1.0000000093132257,
|
||
"cpuTotal": 4,
|
||
"memoryTotal": 1038336000,
|
||
"memoryAvailable": 298635264,
|
||
"memoryUsed": 572526592,
|
||
"memoryUsedPercent": 55.13885601577909,
|
||
"swapMemoryTotal": 4160745472,
|
||
"swapMemoryAvailable": 3816017920,
|
||
"swapMemoryUsed": 344727552,
|
||
"swapMemoryUsedPercent": 8.285235285836778,
|
||
"ioReadBytes": 0,
|
||
"ioWriteBytes": 0,
|
||
"ioCount": 0,
|
||
"ioReadTime": 0,
|
||
"ioWriteTime": 0,
|
||
"diskData": [
|
||
{
|
||
"path": "/",
|
||
"type": "xfs",
|
||
"device": "/dev/mapper/centos-root",
|
||
"total": 37688381440,
|
||
"free": 9805086720,
|
||
"used": 27883294720,
|
||
"usedPercent": 73.98379461954417,
|
||
"inodesTotal": 18411520,
|
||
"inodesUsed": 729745,
|
||
"inodesFree": 17681775,
|
||
"inodesUsedPercent": 3.963523924151836
|
||
},
|
||
{
|
||
"path": "/www/monitor_data",
|
||
"type": "ext4",
|
||
"device": "/dev/vdb1",
|
||
"total": 10432565248,
|
||
"free": 8118497280,
|
||
"used": 1760526336,
|
||
"usedPercent": 17.82085360286682,
|
||
"inodesTotal": 655360,
|
||
"inodesUsed": 10029,
|
||
"inodesFree": 645331,
|
||
"inodesUsedPercent": 1.530303955078125
|
||
}
|
||
],
|
||
"netBytesSent": 0,
|
||
"netBytesRecv": 0,
|
||
"gpuData": null,
|
||
"xpuData": null,
|
||
"shotTime": "2025-06-06T17:01:59.156659566+08:00"
|
||
}
|
||
}"""
|
||
try:
|
||
data = self.lpc.system_status()
|
||
system_data = data.get("data", {})
|
||
return {
|
||
"cpu": [round(system_data["cpuUsedPercent"], 2), system_data["cpuTotal"]],
|
||
"mem": {
|
||
"memRealUsed": system_data["memoryUsed"] / 1024 / 1024,
|
||
"memTotal": system_data["memoryTotal"] / 1024 / 1024,
|
||
"memNewTotal": public.to_size(system_data["memoryTotal"])
|
||
},
|
||
"version": "1Panel"
|
||
}, ""
|
||
|
||
except Exception as e:
|
||
public.print_error()
|
||
return None, str(e)
|
||
|
||
def get_tmp_token(self) -> Tuple[Optional[str], str]:
|
||
return None, "1Panel does not support temporary API access"
|
||
|
||
def php_site_list(self) -> Tuple[List[dict], str]:
|
||
"""{
|
||
"code": 200,
|
||
"message": "",
|
||
"data": [
|
||
{
|
||
"id": 2,
|
||
"createdAt": "2025-06-06T14:49:00.687860227+08:00",
|
||
"updatedAt": "2025-06-06T14:49:00.687860227+08:00",
|
||
"protocol": "HTTP",
|
||
"primaryDomain": "www.halotest.com",
|
||
"type": "deployment",
|
||
"alias": "www.halotest.com",
|
||
"remark": "",
|
||
"status": "Running",
|
||
"httpConfig": "",
|
||
"expireDate": "9999-12-31T00:00:00Z",
|
||
"proxy": "127.0.0.1:8090",
|
||
"proxyType": "",
|
||
"errorLog": true,
|
||
"accessLog": true,
|
||
"defaultServer": false,
|
||
"IPV6": false,
|
||
"rewrite": "",
|
||
"webSiteGroupId": 1,
|
||
"webSiteSSLId": 0,
|
||
"runtimeID": 0,
|
||
"appInstallId": 5,
|
||
"ftpId": 0,
|
||
"parentWebsiteID": 0,
|
||
"user": "",
|
||
"group": "",
|
||
"dbType": "",
|
||
"dbID": 0,
|
||
"favorite": false,
|
||
"domains": [
|
||
{
|
||
"id": 2,
|
||
"createdAt": "2025-06-06T14:49:00.69011631+08:00",
|
||
"updatedAt": "2025-06-06T14:49:00.69011631+08:00",
|
||
"websiteId": 2,
|
||
"domain": "www.halotest.com",
|
||
"ssl": false,
|
||
"port": 7080
|
||
},
|
||
{
|
||
"id": 3,
|
||
"createdAt": "2025-06-06T14:51:47.932141706+08:00",
|
||
"updatedAt": "2025-06-06T14:51:47.932141706+08:00",
|
||
"websiteId": 2,
|
||
"domain": "qwww.com",
|
||
"ssl": false,
|
||
"port": 7080
|
||
}
|
||
],
|
||
"webSiteSSL": {
|
||
"id": 0,
|
||
"createdAt": "0001-01-01T00:00:00Z",
|
||
"updatedAt": "0001-01-01T00:00:00Z",
|
||
"primaryDomain": "",
|
||
"privateKey": "",
|
||
"pem": "",
|
||
"domains": "",
|
||
"certURL": "",
|
||
"type": "",
|
||
"provider": "",
|
||
"organization": "",
|
||
"dnsAccountId": 0,
|
||
"acmeAccountId": 0,
|
||
"caId": 0,
|
||
"autoRenew": false,
|
||
"expireDate": "0001-01-01T00:00:00Z",
|
||
"startDate": "0001-01-01T00:00:00Z",
|
||
"status": "",
|
||
"message": "",
|
||
"keyType": "",
|
||
"pushDir": false,
|
||
"dir": "",
|
||
"description": "",
|
||
"skipDNS": false,
|
||
"nameserver1": "",
|
||
"nameserver2": "",
|
||
"disableCNAME": false,
|
||
"execShell": false,
|
||
"shell": "",
|
||
"acmeAccount": {
|
||
"id": 0,
|
||
"createdAt": "0001-01-01T00:00:00Z",
|
||
"updatedAt": "0001-01-01T00:00:00Z",
|
||
"email": "",
|
||
"url": "",
|
||
"type": "",
|
||
"eabKid": "",
|
||
"eabHmacKey": "",
|
||
"keyType": "",
|
||
"useProxy": false,
|
||
"caDirURL": ""
|
||
},
|
||
"dnsAccount": {
|
||
"id": 0,
|
||
"createdAt": "0001-01-01T00:00:00Z",
|
||
"updatedAt": "0001-01-01T00:00:00Z",
|
||
"name": "",
|
||
"type": ""
|
||
},
|
||
"websites": null
|
||
},
|
||
"errorLogPath": "",
|
||
"accessLogPath": "",
|
||
"sitePath": "",
|
||
"appName": "",
|
||
"runtimeName": "",
|
||
"runtimeType": "",
|
||
"siteDir": ""
|
||
}
|
||
]
|
||
}"""
|
||
try:
|
||
data = self.lpc.get_websites()
|
||
res = []
|
||
for i in data["data"]:
|
||
res.append({
|
||
"site_id": i["id"],
|
||
"site_name": i["alias"],
|
||
"ports": list(set([i["port"] for i in i["domains"]])),
|
||
"domains": list(set([i["domain"] for i in i["domains"]])),
|
||
"ssl": any(i["ssl"] for i in i["domains"])
|
||
})
|
||
return res, ""
|
||
except Exception as e:
|
||
return [], str(e)
|
||
|
||
def create_php_site(self, site_name: str, port: int, **kwargs) -> Tuple[Optional[int], str]:
|
||
try:
|
||
dat = self.lpc.add_website(site_name, port, **kwargs)
|
||
# public.print_log(dat)
|
||
if dat["code"] == 200:
|
||
time.sleep(0.5)
|
||
site_id = self.lpc.check_site_create(site_name)
|
||
if isinstance(site_id, int):
|
||
return site_id, ""
|
||
return None, "Website creation failed"
|
||
return None, dat["message"]
|
||
except Exception as e:
|
||
return None, str(e)
|
||
|
||
def set_firewall_open(self, port: int, protocol: str = "tcp") -> Tuple[bool, str]:
|
||
try:
|
||
dat = self.lpc.open_port(port, protocol)
|
||
if dat["code"] == 200:
|
||
return True, ""
|
||
return False, dat["message"]
|
||
except Exception as e:
|
||
return False, str(e)
|
||
|
||
def add_domain(self, site_id: int, site_name: str, domain: str, port: int) -> Tuple[bool, str]:
|
||
try:
|
||
dat = self.lpc.add_website_domain(site_id, domain, port)
|
||
if dat["code"] == 200:
|
||
return True, ""
|
||
return False, dat["message"]
|
||
except Exception as e:
|
||
return False, str(e)
|
||
|
||
def has_domain(self, site_id: int, domain: str) -> bool:
|
||
try:
|
||
dat = self.lpc.website_domains(site_id)
|
||
if dat["code"] == 200:
|
||
for i in dat["data"]:
|
||
if i["domain"] == domain:
|
||
return True
|
||
return False
|
||
return False
|
||
except Exception as e:
|
||
return False
|
||
|
||
def target_file_exits(self, target_file: str) -> Tuple[bool, str]:
|
||
data = self.lpc.files_exits([target_file])
|
||
if not isinstance(data, dict):
|
||
return False, "Request file: {} status failed".format(target_file)
|
||
for i in data["data"]:
|
||
if i["path"] == target_file:
|
||
return True, ""
|
||
return False, ""
|
||
|
||
def upload_dir_check(self, target_dir: str) -> str:
|
||
data = self.lpc.files_exits([target_dir])
|
||
if not isinstance(data, dict):
|
||
return "Request file: {} status failed".format(target_dir)
|
||
for i in data["data"]:
|
||
if i["path"] == target_dir:
|
||
if not i.get("isDir", True):
|
||
return "The name path is not a directory"
|
||
return ""
|
||
return ""
|
||
|
||
def _upload_big_file(self, filename: str, target_path: str, upload_name: str,
|
||
call_log: Callable[[int, str], None] = None) -> str:
|
||
try:
|
||
fb = open(filename, 'rb')
|
||
except Exception as e:
|
||
public.print_error()
|
||
return "File {} failed to open, please check file permissions, error message is:{}".format(filename, str(e))
|
||
|
||
file_size = os.path.getsize(filename)
|
||
count = math.ceil(file_size / (1024 * 1024 * 5))
|
||
idx = 0
|
||
for i in range(0, file_size, 1024 * 1024 * 5):
|
||
file_data = fb.read(1024 * 1024 * 5)
|
||
err, data = self.lpc.chunkupload(upload_name, target_path, file_data, idx, count)
|
||
idx += 1
|
||
if err:
|
||
return "Upload file {} failed with error message:{}".format(filename, str(err))
|
||
if data:
|
||
call_log(100, "File upload:{} -> {},Upload successful".format(filename, upload_name))
|
||
else:
|
||
up_d = (i + len(file_data)) // file_size
|
||
call_log(up_d, "File upload:{} -> {},The uploaded size is:{}".format(
|
||
filename, upload_name, public.to_size(i + len(file_data))))
|
||
return ""
|
||
|
||
def _upload_little_file(self, filename: str, target_path: str, upload_name: str,
|
||
call_log: Callable[[int, str], None] = None) -> str:
|
||
return self.lpc.upload(filename, target_path, upload_name)
|
||
|
||
def download_file(self, filename: str, target_path: str, mode: str,
|
||
call_log: Callable[[int, str], None] = None) -> str:
|
||
target_file = os.path.join(target_path, os.path.basename(filename))
|
||
exits = os.path.exists(target_file)
|
||
if exits and mode == "ignore":
|
||
call_log(0, "File Download:{} -> {},The target file already exists, skip download".format(filename, target_file))
|
||
return ""
|
||
if exits and mode == "rename":
|
||
download_name = "{}_{}".format(os.path.basename(filename), public.md5(filename))
|
||
call_log(0, "File Download:{} -> {},The target file already exists, it will be renamed to {}".format(filename, target_file, download_name))
|
||
else:
|
||
download_name = os.path.basename(filename)
|
||
|
||
return self.lpc.download_file(filename, target_path, download_name, call_log=call_log)
|
||
|
||
def dir_walk(self, path: str) -> Tuple[List[dict], str]:
|
||
return self.lpc.dir_walk(path)
|
||
|
||
def upload_proxy(self):
|
||
try:
|
||
from YakPanel import request, cache
|
||
f_name = request.form.get('f_name')
|
||
f_path = request.form.get('f_path')
|
||
f_size = request.form.get('f_size')
|
||
f_start = request.form.get('f_start')
|
||
cache_key = "upload_file_{}_{}_{}".format(f_name, f_path, f_size)
|
||
num = cache.get(cache_key)
|
||
if num is None:
|
||
num = 0
|
||
cache.set(cache_key, num, 86400)
|
||
else:
|
||
num = int(num)
|
||
cache.set(cache_key, num + 1, 86400)
|
||
blob_file: FileStorage = request.files.getlist('blob')[0]
|
||
file_data = blob_file.read()
|
||
next_size = int(f_start) + len(file_data)
|
||
if next_size == int(f_size):
|
||
chunk_num = num + 1
|
||
else:
|
||
chunk_num = num + 2
|
||
err, data = self.lpc.chunkupload(f_name, f_path, file_data, num, chunk_num)
|
||
if err:
|
||
return {"status": False, "msg": "Upload file failed with error message:{}".format(str(err))}
|
||
elif data is None:
|
||
return str(next_size)
|
||
elif isinstance(data, dict) and data["code"] == 200:
|
||
return {"status": True, "msg": "Upload successful"}
|
||
return {"status": False, "msg": "Upload file failed with error message:{}".format(data["message"])}
|
||
except Exception as e:
|
||
public.print_error()
|
||
return {"status": False, "msg": "Upload file failed with error message:{}".format(str(e))}
|
||
|
||
def remove_file(self, filename: str, is_dir: bool) -> dict:
|
||
try:
|
||
res = self.lpc.remove_file(filename, is_dir)
|
||
if isinstance(res, dict):
|
||
return {"status": res["code"] == 200, "msg": res["message"]}
|
||
return {"status": False, "msg": "File deletion failed, remote request failed"}
|
||
except Exception as e:
|
||
return {"status": False, "msg": "Failed to delete file, error message is:{}".format(str(e))}
|
||
|
||
def file_list(self, path: str, p: int, row: int, search: str) -> Tuple[Dict, str]:
|
||
try:
|
||
res, err = self.lpc.files_search(path, p, row, search)
|
||
if err:
|
||
return {}, "Failed to retrieve file list, error message is:{}".format(err)
|
||
count = res["itemTotal"]
|
||
path = res["path"]
|
||
dirs = []
|
||
files = []
|
||
sub_items = [] if res["items"] is None else res["items"]
|
||
for i in sub_items:
|
||
# res = self._TIME_REGEXP.findall(i["modTime"])
|
||
# public.print_log(res)
|
||
mt_str = self._TIME_REGEXP.sub(lambda x: x.group().replace(x.group("err"), ""), i["modTime"])
|
||
for f_str in ("%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S.%f%z"):
|
||
try:
|
||
mt = datetime.strptime(mt_str, f_str)
|
||
break
|
||
except:
|
||
pass
|
||
# public.print_log(i["modTime"], mt_str)
|
||
# public.print_error()
|
||
else:
|
||
continue
|
||
|
||
if i["isDir"]:
|
||
dirs.append({
|
||
"nm": html.unescape(i["name"]),
|
||
"sz": i["size"],
|
||
"mt": int(mt.timestamp()),
|
||
"acc": i["mode"][1:],
|
||
"user": i["user"],
|
||
"lnk": "" if not i["linkPath"] else " -> " + i["linkPath"],
|
||
"durl": "",
|
||
"cmp": 0,
|
||
"fav": "0",
|
||
"rmk": "",
|
||
"top": 0,
|
||
"sn": i["name"]
|
||
})
|
||
else:
|
||
files.append({
|
||
"nm": html.unescape(i["name"]),
|
||
"sz": i["size"],
|
||
"mt": int(mt.timestamp()),
|
||
"acc": i["mode"][1:],
|
||
"user": i["user"],
|
||
"lnk": "" if not i["linkPath"] else " -> " + i["linkPath"],
|
||
"durl": "",
|
||
"cmp": 0,
|
||
"fav": "0",
|
||
"rmk": "",
|
||
"top": 0,
|
||
"sn": i["name"]
|
||
})
|
||
|
||
return {
|
||
"path": path,
|
||
"page": public.get_page(count, p, row)["page"],
|
||
"dir": dirs,
|
||
"files": files
|
||
}, ""
|
||
except Exception as e:
|
||
return {}, "Failed to retrieve file list, error message is:{}".format(str(e))
|
||
|
||
def download_proxy(self, filename: str):
|
||
return self.lpc.download_proxy(filename)
|
||
|
||
def upload_check(self, target_file_list: List[str]) -> Tuple[List[dict], str]:
|
||
try:
|
||
res = self.lpc.files_exits(target_file_list)
|
||
if res is None:
|
||
return [], "Request for remote 1Panel failed"
|
||
data = res.get("data", [])
|
||
res_data = []
|
||
for i in data:
|
||
try:
|
||
mt = datetime.strptime(i["modTime"], "%Y-%m-%dT%H:%M:%S%z")
|
||
except:
|
||
try:
|
||
mt = datetime.strptime(i["modTime"], "%Y-%m-%dT%H:%M:%S.%f%z")
|
||
except:
|
||
continue
|
||
res_data.append({
|
||
'filename': i["path"],
|
||
'exists': True,
|
||
'size': i["size"],
|
||
'mtime': int(mt.timestamp()),
|
||
'isfile': False
|
||
})
|
||
|
||
return res_data, ""
|
||
except Exception as e:
|
||
return [], "Request for remote 1Panel failed with error message:{}".format(str(e))
|
||
|
||
def dir_size(self, path: str) -> Tuple[Optional[int], str]:
|
||
try:
|
||
res = self.lpc.dir_size(path)
|
||
if res is None:
|
||
return None, "Request for remote 1Panel failed"
|
||
if not isinstance(res, dict):
|
||
return None, "Request for remote 1Panel failed, response data:%s" % str(res)
|
||
return res["data"].get("size", 0), ""
|
||
except Exception as e:
|
||
return None, "Request for remote 1Panel failed: %s" % str(e)
|
||
|
||
def get_sshd_port(self) -> Optional[int]:
|
||
try:
|
||
data = self.lpc.get_sshd_config()
|
||
return int(data["port"]) if data else None
|
||
except:
|
||
return None
|
||
|
||
def create_dir(self, path: str) -> Tuple[Optional[Dict], str]:
|
||
try:
|
||
data = self.lpc.create_dir(path)
|
||
if not data:
|
||
return None, "Failed to create directory, 1Panel node connection failed"
|
||
status = data["code"] == 200
|
||
return {"status": status, "msg": data["message"] if not status else "Created successfully"}, ""
|
||
except Exception as e:
|
||
return None, "Request for remote 1Panel node failed: %s" % str(e)
|
||
|
||
def restart_bt_panel(self) -> Dict[str, Any]:
|
||
self.lpc.restart_panel()
|
||
return {"status": True, "msg": "Restart successful"}
|
||
|
||
def server_reboot(self) -> Dict[str, Any]:
|
||
self.lpc.server_reboot()
|
||
|
||
from mod.project.node.dbutil import ServerMonitorRepo
|
||
repo = ServerMonitorRepo()
|
||
repo.set_wait_reboot(self.node_server_ip, True)
|
||
|
||
def wait_for_reboot():
|
||
# wait 等待服务器重启成功, 超时时间默认为 10 分钟
|
||
wait_for = time.time() + 600
|
||
time.sleep(3)
|
||
while time.time() < wait_for:
|
||
if self.test_conn() == "": # 无错误时表示重启成功
|
||
repo.set_wait_reboot(self.node_server_ip, False)
|
||
from mod.project.node.dbutil import ServerNodeDB
|
||
node_data = ServerNodeDB().find_node(api_key=self.api_key, app_key=self.app_key.to_string())
|
||
if node_data:
|
||
monitor_node_once(node_data)
|
||
return {"status": True, "msg": "Server restart successful"}
|
||
time.sleep(3)
|
||
# public.print_log("Waiting for server restart... {}".format(wait_for - time.time()))
|
||
repo.set_wait_reboot(self.node_server_ip, False)
|
||
return {"status": False, "msg": "Restarting server failed, server information has not been detected for more than 10 minutes"}
|
||
|
||
t = threading.Thread(target=wait_for_reboot, daemon=True)
|
||
t.start()
|
||
|
||
return {"status": True, "msg": "The server restart has started, please wait patiently for the successful restart"}
|
||
|
||
def get_file_body(self, path: str) -> Tuple[Optional[str], str]:
|
||
data_dict, err = self.lpc.get_file_body(path)
|
||
if err != "":
|
||
return None, err
|
||
return data_dict["content"], ""
|
||
|
||
def read_ssh_key(self) -> Tuple[Optional[str], str]:
|
||
key, err = self.lpc.get_file_body("/root/.ssh/id_ed25519_1panel")
|
||
if err:
|
||
return None, "Failed to read SSH key, error message is: %s" % err
|
||
return key, "" |