450 lines
18 KiB
Python
450 lines
18 KiB
Python
|
|
import json
|
||
|
|
import os.path
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from typing import Tuple, Optional, List, Union
|
||
|
|
|
||
|
|
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 LoadSite:
|
||
|
|
name: str
|
||
|
|
site_name: str
|
||
|
|
site_type: str
|
||
|
|
ps: str = ''
|
||
|
|
http_config: dict = field(default_factory=lambda: {
|
||
|
|
"proxy_next_upstream": "error timeout http_500 http_502 http_503 http_504",
|
||
|
|
"http_alg": "sticky_cookie",
|
||
|
|
"proxy_cache_status": False,
|
||
|
|
"cache_time": "1d",
|
||
|
|
"cache_suffix": "css,js,jpg,jpeg,gif,png,webp,woff,eot,ttf,svg,ico,css.map,js.map",
|
||
|
|
})
|
||
|
|
tcp_config: dict = field(default_factory=lambda: {
|
||
|
|
"proxy_connect_timeout": 8,
|
||
|
|
"proxy_timeout": 86400,
|
||
|
|
"host": "127.0.0.1",
|
||
|
|
"port": 80,
|
||
|
|
"type": "tcp"
|
||
|
|
})
|
||
|
|
created_at: int = 0
|
||
|
|
load_id: int = 0
|
||
|
|
site_id: int = 0
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def bind_http_load(cls, data: dict) -> Tuple[Optional["LoadSite"], str]:
|
||
|
|
check_msg = cls.base_check(data)
|
||
|
|
if check_msg:
|
||
|
|
return None, check_msg
|
||
|
|
if not data.get('site_name', None):
|
||
|
|
return None, 'site_name is required'
|
||
|
|
if not public.is_domain(data['site_name']):
|
||
|
|
return None, 'site_name is invalid'
|
||
|
|
if not isinstance(data.get('http_config', None), dict):
|
||
|
|
return None, 'http_config is required'
|
||
|
|
else:
|
||
|
|
if "proxy_cache_status" not in dict.keys(data['http_config']): #兼容旧版本数据
|
||
|
|
data['http_config']["proxy_cache_status"] = False
|
||
|
|
data['http_config']["cache_time"] = "1d"
|
||
|
|
data['http_config']["cache_suffix"] = "css,js,jpg,jpeg,gif,png,webp,woff,eot,ttf,svg,ico,css.map,js.map"
|
||
|
|
for k in ['proxy_next_upstream', 'http_alg', "proxy_cache_status", "cache_time", "cache_suffix"]:
|
||
|
|
if k not in dict.keys(data['http_config']):
|
||
|
|
return None, 'http_config.{} is required'.format(k)
|
||
|
|
for i in data['http_config']['proxy_next_upstream'].split():
|
||
|
|
if i not in ('error', 'timeout') and not re.match(r'^http_\d{3}$', i):
|
||
|
|
return None, 'http_config.proxy_next_upstream is invalid'
|
||
|
|
if data['http_config']['http_alg'] not in ('sticky_cookie', 'round_robin', 'least_conn', 'ip_hash'):
|
||
|
|
return None, 'http_config.http_alg is invalid'
|
||
|
|
if not isinstance(data['http_config']['proxy_cache_status'], bool):
|
||
|
|
return None, 'http_config.proxy_cache_status is invalid'
|
||
|
|
if not isinstance(data['http_config']['cache_time'], str):
|
||
|
|
return None, 'http_config.cache_time is invalid'
|
||
|
|
if not re.match(r"^[0-9]+([smhd])$", data['http_config']['cache_time']):
|
||
|
|
return None, 'http_config.cache_time is invalid'
|
||
|
|
cache_suffix = data['http_config']['cache_suffix']
|
||
|
|
cache_suffix_list = []
|
||
|
|
for suffix in cache_suffix.split(","):
|
||
|
|
tmp_suffix = re.sub(r"\s", "", suffix)
|
||
|
|
if not tmp_suffix:
|
||
|
|
continue
|
||
|
|
cache_suffix_list.append(tmp_suffix)
|
||
|
|
real_cache_suffix = ",".join(cache_suffix_list)
|
||
|
|
if not real_cache_suffix:
|
||
|
|
real_cache_suffix = "css,js,jpg,jpeg,gif,png,webp,woff,eot,ttf,svg,ico,css.map,js.map"
|
||
|
|
data['http_config']['cache_suffix'] = real_cache_suffix
|
||
|
|
|
||
|
|
l = LoadSite(data.get('name'), data.get('site_name'), 'http', data.get('ps', ''),
|
||
|
|
http_config=data.get('http_config'),
|
||
|
|
created_at=data.get('created_at', 0), load_id=data.get('load_id', 0),
|
||
|
|
site_id=data.get('site_id', 0))
|
||
|
|
return l, ""
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def base_check(cls, data) -> str:
|
||
|
|
if not data.get('name', None):
|
||
|
|
return 'name is required'
|
||
|
|
if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9_]+$', data['name']):
|
||
|
|
return 'The name can only contain letters, numbers, underscores, and cannot start with numbers or underscores'
|
||
|
|
if not len(data['name']) >= 3:
|
||
|
|
return 'The length of the name cannot be less than 3 characters'
|
||
|
|
return ""
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def bind_tcp_load(cls, data: dict) -> Tuple[Optional["LoadSite"], str]:
|
||
|
|
check_msg = cls.base_check(data)
|
||
|
|
if check_msg:
|
||
|
|
return None, check_msg
|
||
|
|
if not isinstance(data.get('tcp_config', None), dict):
|
||
|
|
return None, 'tcp_config is required'
|
||
|
|
else:
|
||
|
|
for k in ['proxy_connect_timeout', 'proxy_timeout', 'host', 'port', 'type']:
|
||
|
|
if not data['tcp_config'].get(k):
|
||
|
|
return None, 'tcp_config.{} is required'.format(k)
|
||
|
|
if data['tcp_config']['type'] not in ('tcp', 'udp'):
|
||
|
|
return None, 'tcp_config.type is invalid'
|
||
|
|
if not isinstance(data['tcp_config']['port'], int) and not 1 <= data['tcp_config']['port'] <= 65535:
|
||
|
|
return None, 'tcp_config.port is invalid'
|
||
|
|
if not public.check_ip(data['tcp_config']['host']):
|
||
|
|
return None, 'tcp_config.host is invalid'
|
||
|
|
|
||
|
|
l = LoadSite(data.get('name'), data.get('site_name'), 'tcp', ps=data.get('ps', ''),
|
||
|
|
tcp_config=data.get('tcp_config'),
|
||
|
|
created_at=data.get('created_at', 0), load_id=data.get('load_id', 0),
|
||
|
|
site_id=data.get('site_id', 0))
|
||
|
|
return l, ""
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
return {
|
||
|
|
"name": self.name,
|
||
|
|
"site_name": self.site_name,
|
||
|
|
"site_type": self.site_type,
|
||
|
|
"ps": self.ps,
|
||
|
|
"http_config": self.http_config,
|
||
|
|
"tcp_config": self.tcp_config,
|
||
|
|
"created_at": self.created_at,
|
||
|
|
"load_id": self.load_id,
|
||
|
|
"site_id": self.site_id
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class HttpNode:
|
||
|
|
node_id: int
|
||
|
|
node_site_name: str
|
||
|
|
port: int
|
||
|
|
location: str = "/"
|
||
|
|
path: str = "/"
|
||
|
|
node_status: str = "online" # online, backup, down
|
||
|
|
weight: int = 1
|
||
|
|
max_fail: int = 3
|
||
|
|
fail_timeout: int = 600
|
||
|
|
ps: str = ""
|
||
|
|
created_at: int = 0
|
||
|
|
node_site_id: int = 0
|
||
|
|
id: int = 0
|
||
|
|
load_id: int = 0
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def bind(cls, data: dict) -> Tuple[Optional["HttpNode"], str]:
|
||
|
|
if not isinstance(data.get('node_site_name', None), str):
|
||
|
|
return None, 'node_site_name is required'
|
||
|
|
if not public.is_domain(data['node_site_name']) and not public.check_ip(data['node_site_name']):
|
||
|
|
return None, 'node_site_name is invalid'
|
||
|
|
if not isinstance(data.get('port', None), int):
|
||
|
|
return None, 'port is required'
|
||
|
|
if not 1 <= data['port'] <= 65535:
|
||
|
|
return None, 'port is invalid'
|
||
|
|
if not isinstance(data.get('node_id', None), int):
|
||
|
|
return None, 'node_id is required'
|
||
|
|
if not isinstance(data.get('node_status', None), str):
|
||
|
|
return None, 'node_status is required'
|
||
|
|
if not data['node_status'] in ('online', 'backup', 'down'):
|
||
|
|
return None, 'node_status is invalid'
|
||
|
|
|
||
|
|
n = HttpNode(data.get('node_id'), data.get('node_site_name'), data.get('port'), "/",
|
||
|
|
data.get('path', "/"), data.get('node_status', "online"), data.get('weight', 1),
|
||
|
|
data.get('max_fail', 3), data.get('fail_timeout', 600), data.get('ps', ''),
|
||
|
|
data.get('created_at', 0), data.get('node_site_id', 0), data.get('id', 0),
|
||
|
|
data.get('load_id', 0)
|
||
|
|
)
|
||
|
|
return n, ""
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
return {
|
||
|
|
"node_id": self.node_id,
|
||
|
|
"node_site_name": self.node_site_name,
|
||
|
|
"port": self.port,
|
||
|
|
"location": self.location,
|
||
|
|
"path": self.path,
|
||
|
|
"node_status": self.node_status,
|
||
|
|
"weight": self.weight,
|
||
|
|
"max_fail": self.max_fail,
|
||
|
|
"fail_timeout": self.fail_timeout,
|
||
|
|
"ps": self.ps,
|
||
|
|
"created_at": self.created_at,
|
||
|
|
"node_site_id": self.node_site_id,
|
||
|
|
"id": self.id,
|
||
|
|
"load_id": self.load_id
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class TcpNode:
|
||
|
|
node_id: int
|
||
|
|
host: str
|
||
|
|
port: int
|
||
|
|
id: int = 0
|
||
|
|
load_id: int = 0
|
||
|
|
node_status: str = "online" # online, backup, down
|
||
|
|
weight: int = 1
|
||
|
|
max_fail: int = 3
|
||
|
|
fail_timeout: int = 600
|
||
|
|
ps: str = ""
|
||
|
|
created_at: int = 0
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def bind(cls, data: dict) -> Tuple[Optional["TcpNode"], str]:
|
||
|
|
if not isinstance(data.get('node_status', None), str):
|
||
|
|
return None, 'node_status is required'
|
||
|
|
if not data['node_status'] in ('online', 'backup', 'down'):
|
||
|
|
return None, 'node_status is invalid'
|
||
|
|
if not isinstance(data.get('host', None), str):
|
||
|
|
return None, 'host is required'
|
||
|
|
if not isinstance(data.get('node_id', None), int):
|
||
|
|
return None, 'node_id is required'
|
||
|
|
if not isinstance(data.get('port', None), int):
|
||
|
|
return None, 'port is required'
|
||
|
|
if not 1 <= data['port'] <= 65535:
|
||
|
|
return None, 'port is invalid'
|
||
|
|
n = TcpNode(data.get('node_id'), data.get('host'), data.get('port'), data.get('id', 0), data.get('load_id', 0),
|
||
|
|
data.get('node_status', "online"), data.get('weight', 1), data.get('max_fail', 3),
|
||
|
|
data.get('fail_timeout', 600), data.get('ps', ''), data.get('created_at', 0))
|
||
|
|
return n, ""
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
return {
|
||
|
|
"node_id": self.node_id,
|
||
|
|
"host": self.host,
|
||
|
|
"port": self.port,
|
||
|
|
"id": self.id,
|
||
|
|
"load_id": self.load_id,
|
||
|
|
"node_status": self.node_status,
|
||
|
|
"weight": self.weight,
|
||
|
|
"max_fail": self.max_fail,
|
||
|
|
"fail_timeout": self.fail_timeout,
|
||
|
|
"ps": self.ps,
|
||
|
|
"created_at": self.created_at
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class NodeDB:
|
||
|
|
_DB_FILE = public.get_panel_path() + "/data/db/node_load_balance.db"
|
||
|
|
_DB_INIT_FILE = os.path.dirname(__file__) + "/load_balancer.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)
|
||
|
|
if not os.path.exists(self._DB_FILE) or os.path.getsize(self._DB_FILE) == 0:
|
||
|
|
public.writeFile(self._DB_FILE, "")
|
||
|
|
import sqlite3
|
||
|
|
conn = sqlite3.connect(self._DB_FILE)
|
||
|
|
c = conn.cursor()
|
||
|
|
c.executescript(sql_data)
|
||
|
|
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 update_load_key(self, load_id: int, load_data: dict) -> str:
|
||
|
|
if not isinstance(load_id, int):
|
||
|
|
return "load_id is required"
|
||
|
|
if not isinstance(load_data, dict):
|
||
|
|
return "load_data is required"
|
||
|
|
err = self.db.table("load_sites").where("load_id = ?", load_id).update(load_data)
|
||
|
|
if isinstance(err, str):
|
||
|
|
return err
|
||
|
|
return ""
|
||
|
|
|
||
|
|
def name_exist(self, name: str) -> bool:
|
||
|
|
return self.db.table("load_sites").where("name = ?", name).count() > 0
|
||
|
|
|
||
|
|
def load_site_name_exist(self, name: str) -> bool:
|
||
|
|
return self.db.table("load_sites").where("site_name = ?", name).count() > 0
|
||
|
|
|
||
|
|
def load_id_exist(self, load_id: int) -> bool:
|
||
|
|
return self.db.table("load_sites").where("load_id = ?", load_id).count() > 0
|
||
|
|
|
||
|
|
def loads_count(self, site_type: str, query: str = "") -> int:
|
||
|
|
if site_type == "http":
|
||
|
|
if not query:
|
||
|
|
return self.db.table("load_sites").where("site_type = ?", "http").count()
|
||
|
|
return self.db.table("load_sites").where(
|
||
|
|
"site_type = ? AND ps like ?", ("http", "%" + query + "%")).count()
|
||
|
|
else:
|
||
|
|
if not query:
|
||
|
|
return self.db.table("load_sites").where("site_type = ?", "tcp").count()
|
||
|
|
return self.db.table("load_sites").where(
|
||
|
|
"site_type = ? AND ps like ?", ("tcp", "%" + query + "%")).count()
|
||
|
|
|
||
|
|
def loads_list(self, site_type: str, offset: int, limit: int, query: str = ""):
|
||
|
|
if site_type == "all":
|
||
|
|
if query:
|
||
|
|
return self.db.table("load_sites").where("ps like ?", "%" + query + "%").limit(limit, offset).select()
|
||
|
|
return self.db.table("load_sites").limit(limit, offset).select()
|
||
|
|
if site_type == "http":
|
||
|
|
if not query:
|
||
|
|
return self.db.table("load_sites").where("site_type = ?", "http").limit(limit, offset).select()
|
||
|
|
return self.db.table("load_sites").where(
|
||
|
|
"site_type = ? AND ps like ?", ("http", "%" + query + "%")).limit(limit, offset).select()
|
||
|
|
else:
|
||
|
|
if not query:
|
||
|
|
return self.db.table("load_sites").where("site_type = ?", "tcp").limit(limit, offset).select()
|
||
|
|
return self.db.table("load_sites").where(
|
||
|
|
"site_type = ? AND ps like ?", ("tcp", "%" + query + "%")).limit(limit, offset).select()
|
||
|
|
|
||
|
|
def create_load(self, site_type: str, load: LoadSite, nodes: List[Union[HttpNode, TcpNode]]) -> str:
|
||
|
|
load_data = load.to_dict()
|
||
|
|
load_data.pop('load_id')
|
||
|
|
load_data.pop('created_at')
|
||
|
|
load_data["http_config"] = json.dumps(load.http_config)
|
||
|
|
load_data["tcp_config"] = json.dumps(load.tcp_config)
|
||
|
|
try:
|
||
|
|
err = self.db.table("load_sites").insert(load_data)
|
||
|
|
if isinstance(err, str):
|
||
|
|
return err
|
||
|
|
load.load_id = err
|
||
|
|
|
||
|
|
for node in nodes:
|
||
|
|
node_data = node.to_dict()
|
||
|
|
node_data.pop('id')
|
||
|
|
node_data.pop('created_at')
|
||
|
|
node_data['load_id'] = load.load_id
|
||
|
|
if site_type == "http" and isinstance(node, HttpNode):
|
||
|
|
err = self.db.table("http_nodes").insert(node_data)
|
||
|
|
else:
|
||
|
|
err = self.db.table("tcp_nodes").insert(node_data)
|
||
|
|
if isinstance(err, str):
|
||
|
|
return err
|
||
|
|
except Exception as e:
|
||
|
|
return "数据库操作错误:" + str(e)
|
||
|
|
|
||
|
|
return ""
|
||
|
|
|
||
|
|
def update_load(self, site_type: str, load: LoadSite, nodes: List[Union[HttpNode, TcpNode]]) -> str:
|
||
|
|
load_data = load.to_dict()
|
||
|
|
if not load.load_id:
|
||
|
|
return "load_id is required"
|
||
|
|
load_data.pop('created_at')
|
||
|
|
load_data.pop('load_id')
|
||
|
|
load_data["http_config"] = json.dumps(load.http_config)
|
||
|
|
load_data["tcp_config"] = json.dumps(load.tcp_config)
|
||
|
|
|
||
|
|
try:
|
||
|
|
err = self.db.table("load_sites").where("load_id = ?", load.load_id).update(load_data)
|
||
|
|
if isinstance(err, str):
|
||
|
|
return err
|
||
|
|
except Exception as e:
|
||
|
|
return "数据库操作错误:" + str(e)
|
||
|
|
|
||
|
|
old_nodes, err = self.get_nodes(load.load_id, site_type)
|
||
|
|
if err:
|
||
|
|
return err
|
||
|
|
old_nodes_map = {}
|
||
|
|
for old_node in old_nodes:
|
||
|
|
old_nodes_map[old_node['id']] = old_node
|
||
|
|
|
||
|
|
try:
|
||
|
|
for node in nodes:
|
||
|
|
node_data = node.to_dict()
|
||
|
|
node_data.pop('id')
|
||
|
|
node_data.pop('created_at')
|
||
|
|
node_data['load_id'] = load.load_id
|
||
|
|
if node.id in old_nodes_map:
|
||
|
|
if site_type == "http" and isinstance(node, HttpNode):
|
||
|
|
err = self.db.table("http_nodes").where("id = ?", node.id).update(node_data)
|
||
|
|
else:
|
||
|
|
err = self.db.table("tcp_nodes").where("id = ?", node.id).update(node_data)
|
||
|
|
if isinstance(err, str):
|
||
|
|
return err
|
||
|
|
old_nodes_map.pop(node.id)
|
||
|
|
else:
|
||
|
|
if site_type == "http" and isinstance(node, HttpNode):
|
||
|
|
err = self.db.table("http_nodes").insert(node_data)
|
||
|
|
else:
|
||
|
|
err = self.db.table("tcp_nodes").insert(node_data)
|
||
|
|
if isinstance(err, str):
|
||
|
|
return err
|
||
|
|
for node_id in old_nodes_map:
|
||
|
|
if site_type == "http":
|
||
|
|
err = self.db.table("http_nodes").where("id = ?", node_id).delete()
|
||
|
|
else:
|
||
|
|
err = self.db.table("tcp_nodes").where("id = ?", node_id).delete()
|
||
|
|
if isinstance(err, str):
|
||
|
|
return err
|
||
|
|
except Exception as e:
|
||
|
|
return "数据库操作错误:" + str(e)
|
||
|
|
return ""
|
||
|
|
|
||
|
|
def get_nodes(self, load_id: int, site_type: str) -> Tuple[List[dict], str]:
|
||
|
|
if site_type == "http":
|
||
|
|
nodes: List[dict] = self.db.table("http_nodes").where("load_id = ?", load_id).select()
|
||
|
|
else:
|
||
|
|
nodes: List[dict] = self.db.table("tcp_nodes").where("load_id = ?", load_id).select()
|
||
|
|
if isinstance(nodes, str):
|
||
|
|
return [], nodes
|
||
|
|
if not nodes and self.db.ERR_INFO:
|
||
|
|
return [], self.db.ERR_INFO
|
||
|
|
return nodes, ""
|
||
|
|
|
||
|
|
def get_load(self, load_id: int) -> Tuple[Optional[dict], str]:
|
||
|
|
load_data = self.db.table("load_sites").where("load_id = ?", load_id).find()
|
||
|
|
if isinstance(load_data, str):
|
||
|
|
return None, load_data
|
||
|
|
if self.db.ERR_INFO:
|
||
|
|
return None, self.db.ERR_INFO
|
||
|
|
if len(load_data) == 0:
|
||
|
|
return None, "未查询到该负载配置"
|
||
|
|
return load_data, ""
|
||
|
|
|
||
|
|
def delete(self, load_id: int) -> str:
|
||
|
|
load_data = self.db.table("load_sites").where("load_id = ?", load_id).find()
|
||
|
|
if isinstance(load_data, str):
|
||
|
|
return load_data
|
||
|
|
if self.db.ERR_INFO:
|
||
|
|
return self.db.ERR_INFO
|
||
|
|
if len(load_data) == 0:
|
||
|
|
return ""
|
||
|
|
|
||
|
|
if load_data["site_type"] == "http":
|
||
|
|
err = self.db.table("http_nodes").where("load_id = ?", load_id).delete()
|
||
|
|
else:
|
||
|
|
err = self.db.table("tcp_nodes").where("load_id = ?", load_id).delete()
|
||
|
|
if isinstance(err, str):
|
||
|
|
return err
|
||
|
|
err = self.db.table("load_sites").where("load_id = ?", load_id).delete()
|
||
|
|
if isinstance(err, str):
|
||
|
|
return err
|
||
|
|
return ""
|