Files
yakpanel-core/mod/project/node/dbutil/executor.py
2026-04-07 02:04:22 +05:30

482 lines
16 KiB
Python

import os
import sys
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, List, Dict, Tuple, Any, Union, Type, Generic, TypeVar, TextIO
import sqlite3
import json
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 Script:
"""对应scripts表"""
name: str
script_type: str
content: str
id: Optional[int] = None
description: Optional[str] = None
group_id: int = 0
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
@staticmethod
def check(data: Dict[str, Any]) -> str:
if "script_type" not in data or not data["script_type"]:
return "Script type cannot be empty"
if not data["script_type"] in ["python", "shell"]:
return "Script type error, please choose Python or Shell"
if "content" not in data or not data["content"]:
return "Script content cannot be empty"
if "name" not in data or not data["name"]:
return "Script name cannot be empty"
return ""
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Script':
"""从字典创建Script实例"""
return cls(
id=int(data['id']) if data.get('id', None) else None,
name=str(data['name']),
script_type=str(data['script_type']),
content=str(data['content']),
description=str(data['description']) if data.get('description', None) else None,
group_id=int(data['group_id']) if data.get('group_id', None) else 0,
created_at=datetime.fromisoformat(data['created_at']) if data.get('created_at', None) else None,
updated_at=datetime.fromisoformat(data['updated_at']) if data.get('updated_at', None) else None
)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
'id': self.id,
'name': self.name,
'script_type': self.script_type,
'content': self.content,
'description': self.description,
'group_id': self.group_id,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
@dataclass
class ScriptGroup:
"""对应script_groups表"""
name: str
id: Optional[int] = None
description: Optional[str] = None
created_at: Optional[datetime] = None
@staticmethod
def check(data: Dict[str, Any]) -> str:
if "name" not in data or not data["name"]:
return "Script group name cannot be empty"
return ""
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'ScriptGroup':
"""从字典创建ScriptGroup实例"""
return cls(
id=int(data['id']) if data.get('id', None) else None,
name=str(data['name']),
description=str(data['description']) if data.get('description', None) else None,
created_at=datetime.fromisoformat(data['created_at']) if data.get('created_at', None) else None
)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
'id': self.id,
'name': self.name,
'description': self.description,
'created_at': self.created_at.isoformat() if self.created_at else None
}
@dataclass
class ExecutorTask:
"""对应executor_tasks表"""
script_id: int
script_content: str
script_type: str
server_ids: str = ""
id: Optional[int] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
_elogs: Optional[List["ExecutorLog"]] = None
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'ExecutorTask':
"""从字典创建ExecutorTask实例"""
return cls(
id=int(data['id']) if data.get('id', None) else None,
script_id=int(data['script_id']),
script_content=str(data['script_content']),
script_type=str(data['script_type']),
created_at=datetime.fromisoformat(data['created_at']) if data.get('created_at', None) else None,
updated_at=datetime.fromisoformat(data['updated_at']) if data.get('updated_at', None) else None
)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
'id': self.id,
'script_id': self.script_id,
'server_ids': self.server_ids,
'script_content': self.script_content,
'script_type': self.script_type,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
@property
def elogs(self) -> List["ExecutorLog"]:
if self._elogs is None:
return []
return self._elogs
@elogs.setter
def elogs(self, elogs: List["ExecutorLog"]):
self._elogs = elogs
_EXECUTOR_LOG_DIR = public.get_panel_path() + "/logs/executor_log/"
try:
if not os.path.exists(_EXECUTOR_LOG_DIR):
os.makedirs(_EXECUTOR_LOG_DIR)
except:
pass
@dataclass
class ExecutorLog:
"""对应executor_logs表"""
executor_task_id: int
server_id: int
ssh_host: str
id: Optional[int] = None
status: int = 0 # 0:运行中 1:成功 2:失败 3:异常
log_name: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
_log_fp: Optional[TextIO] = None
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'ExecutorLog':
"""从字典创建ExecutorLog实例"""
return cls(
id=int(data['id']) if data.get('id', None) else None,
executor_task_id=int(data['executor_task_id']),
server_id=int(data['server_id']),
ssh_host=str(data['ssh_host']),
status=int(data['status']) if data.get('status', 0) else 0,
log_name=str(data['log_name']) if data.get('log_name', None) else None,
created_at=datetime.fromisoformat(data['created_at']) if data.get('created_at', None) else None,
updated_at=datetime.fromisoformat(data['updated_at']) if data.get('updated_at', None) else None
)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
'id': self.id,
'executor_task_id': self.executor_task_id,
'server_id': self.server_id,
'ssh_host': self.ssh_host,
'status': self.status,
'log_name': self.log_name,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
@property
def log_file(self):
return os.path.join(_EXECUTOR_LOG_DIR, self.log_name)
@property
def log_fp(self):
if self._log_fp is None:
self._log_fp = open(self.log_file, "w+")
return self._log_fp
def create_log(self):
public.writeFile(self.log_file, "")
def remove_log(self):
if os.path.exists(self.log_file):
os.remove(self.log_file)
def get_log(self):
return public.readFile(self.log_file)
def write_log(self, log_data: str, is_end_log=False):
self.log_fp.write(log_data)
self.log_fp.flush()
if is_end_log:
self.log_fp.close()
self._log_fp = None
_TableType = TypeVar("_TableType", bound=Union[Script, ScriptGroup, ExecutorTask, ExecutorLog])
class _Table(Generic[_TableType]):
"""数据库表"""
table_name: str = ""
data_cls: Type[_TableType]
def __init__(self, db_obj: db.Sql):
self._db = db_obj
# 当仅传递一个数据时,返回插入数的 id或错误信息; 当传递多个数据时,返回插入的行数或错误信息
def create(self,
data: Union[_TableType, List[_TableType]]) -> Union[int, str]:
"""创建数据"""
if not isinstance(data, list):
data = [data]
if not len(data):
raise ValueError("Data cannot be empty")
if not isinstance(data[0], self.data_cls):
raise ValueError("Data type error")
now = datetime.now().isoformat()
def fileter_data(item):
item_dict = item.to_dict()
if "id" in item_dict:
item_dict.pop("id")
if "created_at" in item_dict and item_dict["created_at"] is None:
item_dict["created_at"] = now
if "updated_at" in item_dict and item_dict["updated_at"] is None:
item_dict["updated_at"] = now
return item_dict
data_list = list(map(fileter_data, data))
if len(data_list) == 1:
try:
res = self._db.table(self.table_name).insert(data_list[0])
if isinstance(res, int):
return res
return str(res)
except Exception as e:
return str(e)
try:
res = self._db.table(self.table_name).batch_insert(data_list)
if isinstance(res, (int, bool)):
return len(data)
return str(res)
except Exception as e:
return str(e)
def update(self, data: _TableType) -> str:
"""更新数据"""
if not isinstance(data, self.data_cls):
raise ValueError("Data type error")
data_dict = data.to_dict()
data_dict.pop('created_at', None)
if "updated_at" in data_dict:
data_dict["updated_at"] = datetime.now().isoformat()
if "id" not in data_dict:
raise ValueError("The data ID cannot be empty")
try:
self._db.table(self.table_name).where("id=?", (data_dict["id"],)).update(data_dict)
except Exception as e:
return str(e)
return ""
def get_byid(self, data_id: int) -> Optional[_TableType]:
"""根据id获取数据"""
try:
result = self._db.table(self.table_name).where("id=?", (data_id,)).find()
except Exception as e:
return None
if not result:
return None
return self.data_cls.from_dict(result)
def delete(self, data_id: Union[int, List[int]]):
"""删除数据"""
if isinstance(data_id, list):
data_id = [int(item) for item in data_id]
elif isinstance(data_id, int):
data_id = [int(data_id)]
else:
return "数据id类型错误"
try:
self._db.table(self.table_name).where(
"id in ({})".format(",".join(["?"] * len(data_id))), (*data_id,)
).delete()
return ""
except Exception as e:
return str(e)
def query(self, *args) -> List[_TableType]:
"""查询数据"""
try:
result = self._db.table(self.table_name).where(*args).select()
except Exception as e:
return []
if not result:
return []
return [self.data_cls.from_dict(item) for item in result]
def query_page(self, *args, page_num: int = 1, limit: int = 10) -> List[_TableType]:
"""查询数据, 支持分页"""
try:
offset = limit * (page_num - 1)
result = self._db.table(self.table_name).where(*args).limit(limit, offset).order("id DESC").select()
except Exception as e:
public.print_error()
return []
if not result:
return []
return [self.data_cls.from_dict(item) for item in result]
def count(self, *args) -> int:
"""查询数据数量"""
try:
result = self._db.table(self.table_name).where(*args).count()
except Exception as e:
return 0
return result
def find(self, *args) -> Optional[_TableType]:
"""查询单条数据"""
try:
result = self._db.table(self.table_name).where(*args).find()
except Exception as e:
return None
if not result:
return None
return self.data_cls.from_dict(result)
class _ScriptTable(_Table[Script]):
"""脚本表"""
table_name = "scripts"
data_cls = Script
def set_group_id(self, group_id: int, *where_args) -> str:
"""设置脚本组"""
try:
self._db.table(self.table_name).where(where_args).update({"group_id": group_id})
except Exception as e:
return str(e)
return ""
class _ScriptGroupTable(_Table[ScriptGroup]):
"""脚本组表"""
table_name = "script_groups"
data_cls = ScriptGroup
default_group = ScriptGroup(
id=0,
name="default",
description="Default grouping, use this grouping when not set",
created_at=datetime.now(),
)
def all_group(self) -> List[ScriptGroup]:
"""获取所有脚本组"""
try:
result = self._db.table(self.table_name).select()
except Exception as e:
return []
if not result:
return []
return [self.default_group] + [self.data_cls.from_dict(item) for item in result]
class _ExecutorTaskTable(_Table[ExecutorTask]):
"""执行任务表"""
table_name = "executor_tasks"
data_cls = ExecutorTask
def query_tasks(self,
page=1, size=10, node_id: int = None, script_type: str = None, search: str = None
) -> Tuple[int, List[ExecutorTask]]:
"""查询任务"""
where_args, parms = [], []
if script_type and script_type != "all":
where_args.append("script_type=?")
parms.append(script_type)
if search:
search_str = "script_content like ?"
parms.append("%{}%".format(search))
stable = _ScriptTable(self._db)
data = stable.query("name like ? or description like ?", ("%{}%".format(search), "%{}%".format(search)))
if data:
search_str += " or script_id in ({})".format(",".join(["?"] * len(data)))
where_args.append("(" + search_str + ")")
parms.append(tuple([item.id for item in data]))
else:
where_args.append(search_str)
if node_id:
where_args.append("server_ids like ?")
parms.append("%|{}%".format(node_id))
# public.print_log("search criteria: {}".format(" AND ".join(where_args)), parms)
count = self.count(
" AND ".join(where_args),
(*parms, )
)
return count, self.query_page(
" AND ".join(where_args),
(*parms, ),
page_num=page,
limit=size
)
class _ExecutorLogTable(_Table[ExecutorLog]):
"""执行日志表"""
table_name = "executor_logs"
data_cls = ExecutorLog
class ExecutorDB:
_DB_FILE = public.get_panel_path() + "/data/db/executor.db"
_DB_INIT_FILE = os.path.dirname(__file__) + "/executor.sql"
def __init__(self):
sql = db.Sql()
sql._Sql__DB_FILE = self._DB_FILE
self.db = sql
self.Script = _ScriptTable(self.db)
self.ScriptGroup = _ScriptGroupTable(self.db)
self.ExecutorTask = _ExecutorTaskTable(self.db)
self.ExecutorLog = _ExecutorLogTable(self.db)
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)
cursor = conn.cursor()
cursor.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()