Initial YakPanel commit

This commit is contained in:
Niranjan
2026-04-07 02:04:22 +05:30
commit 2826d3e7f3
5359 changed files with 1390724 additions and 0 deletions

View File

@@ -0,0 +1,495 @@
import json
import os
from typing import Dict, Union
from .mods import TaskConfig, TaskTemplateConfig, TaskRecordConfig, SenderConfig, load_task_template_by_config, \
load_task_template_by_file, UPDATE_MOD_PUSH_FILE, UPDATE_VERSION_FILE, PUSH_DATA_PATH
from .base_task import BaseTask
from .send_tool import WxAccountMsg, WxAccountLoginMsg, WxAccountMsgBase
from .system import PushSystem, get_push_public_data, push_by_task_keyword, push_by_task_id
from .manager import PushManager
from .util import read_file, write_file
__all__ = [
"TaskConfig",
"TaskTemplateConfig",
"TaskRecordConfig",
"SenderConfig",
"load_task_template_by_config",
"load_task_template_by_file",
"BaseTask",
"WxAccountMsg",
"WxAccountLoginMsg",
"WxAccountMsgBase",
"PushSystem",
"get_push_public_data",
"PushManager",
"push_by_task_keyword",
"push_by_task_id",
"UPDATE_MOD_PUSH_FILE",
"update_mod_push_system",
"UPDATE_VERSION_FILE",
"PUSH_DATA_PATH",
"get_default_module_dict",
]
def update_mod_push_system():
if os.path.exists(UPDATE_MOD_PUSH_FILE):
return
# 只将已有的告警任务("site_push", "system_push", "database_push") 移动
try:
push_data = json.loads(read_file("/www/server/panel/class/push/push.json"))
except:
return
if not isinstance(push_data, dict):
return
pmgr = PushManager()
default_module_dict = get_default_module_dict()
for key, value in push_data.items():
if key == "site_push":
_update_site_push(value, pmgr, default_module_dict)
elif key == "system_push":
_update_system_push(value, pmgr, default_module_dict)
elif key == "database_push":
_update_database_push(value, pmgr, default_module_dict)
elif key == "rsync_push":
_update_rsync_push(value, pmgr, default_module_dict)
elif key == "load_balance_push":
_update_load_push(value, pmgr, default_module_dict)
elif key == "task_manager_push":
_update_task_manager_push(value, pmgr, default_module_dict)
write_file(UPDATE_MOD_PUSH_FILE, "")
def get_default_module_dict():
res = {}
# wx_account_list = []
for data in SenderConfig().config:
if not data["used"]:
continue
if data.get("original", False):
res[data["sender_type"]] = data["id"]
if data["sender_type"] == "webhook":
res[data["data"].get("title")] = data["id"]
# if data["sender_type"] == "wx_account":
# wx_account_list.append(data)
# wx_account_list.sort(key=lambda x: x.get("data", {}).get("create_time", ""))
# if wx_account_list:
# res["wx_account"] = wx_account_list[0]["id"]
return res
def _update_site_push(old_data: Dict[str, Dict[str, Union[str, int, float, list]]],
pmgr: PushManager,
df_mdl: Dict[str, str]):
for k, v in old_data.items():
sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl]
if v["type"] == "ssl":
push_data = {
"template_id": "1",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"project": v.get("project", "all"),
"cycle": v.get("cycle", 15)
},
"number_rule": {
"total": v.get("push_count", 1)
}
}
}
pmgr.set_task_conf_data(push_data)
elif v["type"] == "site_endtime":
push_data = {
"template_id": "2",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"cycle": v.get("cycle", 7)
},
"number_rule": {
"total": v.get("push_count", 1)
}
}
}
pmgr.set_task_conf_data(push_data)
elif v["type"] == "panel_pwd_endtime":
push_data = {
"template_id": "3",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"cycle": v.get("cycle", 15),
"interval": 600
},
"number_rule": {
"total": v.get("push_count", 1)
}
}
}
pmgr.set_task_conf_data(push_data)
elif v["type"] == "ssh_login_error":
push_data = {
"template_id": "4",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"cycle": v.get("cycle", 30),
"count": v.get("count", 3),
"interval": v.get("interval", 600)
},
"number_rule": {
"day_num": v.get("day_limit", 3)
}
}
}
pmgr.set_task_conf_data(push_data)
elif v["type"] == "services":
push_data = {
"template_id": "5",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"project": v.get("project", "nginx"),
"count": v.get("count", 3),
"interval": v.get("interval", 600)
},
"number_rule": {
"day_num": v.get("day_limit", 3)
}
}
}
pmgr.set_task_conf_data(push_data)
elif v["type"] == "panel_safe_push":
push_data = {
"template_id": "6",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {},
"number_rule": {
"day_num": v.get("day_limit", 3)
}
}
}
pmgr.set_task_conf_data(push_data)
elif v["type"] == "ssh_login":
push_data = {
"template_id": "7",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {},
"number_rule": {}
}
}
pmgr.set_task_conf_data(push_data)
elif v["type"] == "panel_login":
push_data = {
"template_id": "8",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {},
"number_rule": {}
}
}
pmgr.set_task_conf_data(push_data)
elif v["type"] == "project_status":
push_data = {
"template_id": "9",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"cycle": v.get("cycle", 1),
"project": v.get("project", 0),
"count": v.get("count", 2) if v.get("count", 2) not in (1, 2) else 2,
"interval": v.get("interval", 600)
},
"number_rule": {
"day_num": v.get("push_count", 3)
}
}
}
pmgr.set_task_conf_data(push_data)
elif v["type"] == "panel_update":
push_data = {
"template_id": "10",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {},
"number_rule": {
"day_num": 1
}
}
}
pmgr.set_task_conf_data(push_data)
send_type = None
login_send_type_conf = "/www/server/panel/data/panel_login_send.pl"
if os.path.exists(login_send_type_conf):
send_type = read_file(login_send_type_conf).strip()
else:
# user_info["server_id"]之前的
if os.path.exists("/www/server/panel/data/login_send_type.pl"):
send_type = read_file("/www/server/panel/data/login_send_type.pl")
else:
if os.path.exists('/www/server/panel/data/login_send_mail.pl'):
send_type = "mail"
if os.path.exists('/www/server/panel/data/login_send_dingding.pl'):
send_type = "dingding"
if isinstance(send_type, str):
sender_list = [df_mdl[i.strip()] for i in send_type.split(",") if i.strip() in df_mdl]
push_data = {
"template_id": "8",
"task_data": {
"status": True,
"sender": sender_list,
"task_data": {},
"number_rule": {}
}
}
pmgr.set_task_conf_data(push_data)
login_send_type_conf = "/www/server/panel/data/ssh_send_type.pl"
if os.path.exists(login_send_type_conf):
ssh_send_type = read_file(login_send_type_conf).strip()
if isinstance(ssh_send_type, str):
sender_list = [df_mdl[i.strip()] for i in ssh_send_type.split(",") if i.strip() in df_mdl]
push_data = {
"template_id": "7",
"task_data": {
"status": True,
"sender": sender_list,
"task_data": {},
"number_rule": {}
}
}
pmgr.set_task_conf_data(push_data)
return
def _update_system_push(old_data: Dict[str, Dict[str, Union[str, int, float, list]]],
pmgr: PushManager,
df_mdl: Dict[str, str]):
for k, v in old_data.items():
sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl]
if v["type"] == "disk":
push_data = {
"template_id": "20",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"project": v.get("project", "/"),
"cycle": v.get("cycle", 2) if v.get("cycle", 2) not in (1, 2) else 2,
"count": v.get("count", 80),
},
"number_rule": {
"total": v.get("push_count", 3)
}
}
}
pmgr.set_task_conf_data(push_data)
if v["type"] == "disk":
push_data = {
"template_id": "21",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"cycle": v.get("cycle", 5) if v.get("cycle", 5) not in (3, 5, 15) else 5,
"count": v.get("count", 80),
},
"number_rule": {
"total": v.get("push_count", 3)
}
}
}
pmgr.set_task_conf_data(push_data)
if v["type"] == "load":
push_data = {
"template_id": "22",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"cycle": v.get("cycle", 5) if v.get("cycle", 5) not in (1, 5, 15) else 5,
"count": v.get("count", 80),
},
"number_rule": {
"total": v.get("push_count", 3)
}
}
}
pmgr.set_task_conf_data(push_data)
if v["type"] == "mem":
push_data = {
"template_id": "23",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"cycle": v.get("cycle", 5) if v.get("cycle", 5) not in (3, 5, 15) else 5,
"count": v.get("count", 80),
},
"number_rule": {
"total": v.get("push_count", 3)
}
}
}
pmgr.set_task_conf_data(push_data)
return
def _update_database_push(old_data: Dict[str, Dict[str, Union[str, int, float, list]]],
pmgr: PushManager,
df_mdl: Dict[str, str]):
for k, v in old_data.items():
sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl]
if v["type"] == "mysql_pwd_endtime":
push_data = {
"template_id": "30",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"project": v.get("project", []),
"cycle": v.get("cycle", 15),
},
"number_rule": {}
}
}
pmgr.set_task_conf_data(push_data)
elif v["type"] == "mysql_replicate_status":
push_data = {
"template_id": "31",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"project": v.get("project", []),
"count": v.get("cycle", 15),
"interval": v.get("interval", 600)
},
"number_rule": {}
}
}
pmgr.set_task_conf_data(push_data)
return None
def _update_rsync_push(
old_data: Dict[str, Dict[str, Union[str, int, float, list]]],
pmgr: PushManager,
df_mdl: Dict[str, str]):
for k, v in old_data.items():
sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl]
push_data = {
"template_id": "40",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"interval": v.get("interval", 600)
},
"number_rule": {
"day_num": v.get("push_count", 3)
}
}
}
pmgr.set_task_conf_data(push_data)
def _update_load_push(
old_data: Dict[str, Dict[str, Union[str, int, float, list]]],
pmgr: PushManager,
df_mdl: Dict[str, str]):
for k, v in old_data.items():
sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl]
push_data = {
"template_id": "50",
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"project": v.get("project", ""),
"cycle": v.get("cycle", "200|301|302|403|404")
},
"number_rule": {
"day_num": v.get("push_count", 2)
}
}
}
pmgr.set_task_conf_data(push_data)
def _update_task_manager_push(
old_data: Dict[str, Dict[str, Union[str, int, float, list]]],
pmgr: PushManager,
df_mdl: Dict[str, str]):
for k, v in old_data.items():
sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl]
template_id_dict = {
"task_manager_cpu": "60",
"task_manager_mem": "61",
"task_manager_process": "62"
}
if v["type"] in template_id_dict:
push_data = {
"template_id": template_id_dict[v["type"]],
"task_data": {
"status": bool(v.get("status", True)),
"sender": sender_list,
"task_data": {
"project": v.get("project", ""),
"count": v.get("count", 80),
"interval": v.get("count", 600),
},
"number_rule": {
"day_num": v.get("push_count", 3)
}
}
}
pmgr.set_task_conf_data(push_data)

View File

@@ -0,0 +1,215 @@
from typing import Union, Optional, List, Tuple
from .send_tool import WxAccountMsg
# 告警系统在处理每个任务时都会重新建立有一个Task的对象(请勿在__init__的初始化函数中添加任何参数)
# 故每个对象中都可以大胆存放本任务所有数据,不会影响同类型的其他任务
class BaseTask:
def __init__(self):
self.source_name: str = ''
self.title: str = '' # 这个是告警任务的标题(根据实际情况改变)
self.template_name: str = '' # 这个告警模板的标题(不会改变)
def check_task_data(self, task_data: dict) -> Union[dict, str]:
"""
检查设置的告警参数(是否合理)
@param task_data: 传入的告警参数,提前会经过默认值处理(即没有的字段添加默认值)
@return: 当检查无误时,返回一个 dict 当做后续的添加和修改的数据,
当检查有误时, 直接返回错误信息的字符串
"""
raise NotImplementedError()
def get_keyword(self, task_data: dict) -> str:
"""
返回一个关键字,用于后续查询或执行任务时使用, 例如防篡改告警可以根据其规则id生成一个关键字
后续通过规则id和来源tamper 查询并使用
@param task_data: 通过check_args后生成的告警参数字典
@return: 返回一个关键词字符串
"""
raise NotImplementedError()
def get_title(self, task_data: dict) -> str:
"""
返回一个标题
@param task_data: 通过check_args后生成的告警参数字典
@return: 返回一个关键词字符串
"""
if self.title:
return self.title
return self.template_name
def task_run_end_hook(self, res: dict) -> None:
"""
在告警系统中。执行完了任务后,会去掉用这个函数
@type res: dict, 执行任务的结果
@return:
"""
return
def task_config_update_hook(self, task: dict) -> None:
"""
在告警管理中。更新任务数据后,会去掉用这个函数
@return:
"""
return
def task_config_remove_hook(self, task: dict) -> None:
"""
在告警管理中。移除这个任务后,会去掉用这个函数
@return:
"""
return
def task_config_create_hook(self, task: dict) -> None:
"""
在告警管理中。新建这个任务后,会去掉用这个函数
@return:
"""
return
def check_time_rule(self, time_rule: dict) -> Union[dict, str]:
"""
检查和修改设置的告警的时间控制参数是是否合理
可以添加参数 get_by_func 字段用于指定使用本类中的那个函数执行时间判断标准, 替换标准的时间规则判断功能
↑示例如本类中的: can_send_by_time_rule
@param time_rule: 传入的告警参数,提前会经过默认值处理(即没有的字段添加默认值)
@return: 当检查无误时,返回一个 dict 当做后续的添加和修改的数据,
当检查有误时, 直接返回错误信息的字符串
"""
return time_rule
def check_num_rule(self, num_rule: dict) -> Union[dict, str]:
"""
检查和修改设置的告警的次数控制参数是是否合理
可以添加参数 get_by_func 字段用于指定使用本类中的那个函数执行次数判断标准, 替换标准的次数规则判断功能
↑示例如本类中的: can_send_by_num_rule
@param num_rule: 传入的告警参数,提前会经过默认值处理(即没有的字段添加默认值)
@return: 当检查无误时,返回一个 dict 当做后续的添加和修改的数据,
当检查有误时, 直接返回错误信息的字符串
"""
return num_rule
def can_send_by_num_rule(self, task_id: str, task_data: dict, number_rule: dict, push_data: dict) -> Optional[str]:
"""
这是一个通过函数判断是否能够发送告警的示例,并非每一个告警任务都需要有
@param task_id: 任务id
@param task_data: 告警参数信息
@param number_rule: 次数控制信息
@param push_data: 本次要发送的告警信息的原文,应当为字典, 来自 get_push_data 函数的返回值
@return: 返回None
"""
return None
def can_send_by_time_rule(self, task_id: str, task_data: dict, time_rule: dict, push_data: dict) -> Optional[str]:
"""
这是一个通过函数判断是否能够发送告警的示例,并非每一个告警任务都需要有
@param task_id: 任务id
@param task_data: 告警参数信息
@param time_rule: 时间控制信息
@param push_data: 本次要发送的告警信息的原文,应当为字典, 来自 get_push_data 函数的返回值
@return:
"""
return None
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
"""
判断这个任务是否需要返送
@param task_id: 任务id
@param task_data: 任务的告警参数
@return: 如果触发了告警返回一个dict的原文作为告警信息否则应当返回None表示未触发
返回之中应当包含一个 msg_list 的键值为List[str]类型),将主要的信息返回
用于以下信息的自动序列化包含[dingding, feishu, mail, weixin, web_hook]
短信和微信公众号由于长度问题,必须每个任务手动实现
"""
raise NotImplementedError()
def filter_template(self, template: dict) -> Optional[dict]:
"""
过滤 和 更改模板中的信息, 返回空表是当前无法设置该任务
@param template: 任务的模板信息
@return:
"""
raise NotImplementedError()
# push_public_data 公共的告警参数提取位置
# 内容包含:
# ip 网络ip
# local_ip 本机ip
# time 时间日志的字符串
# timestamp 当前的时间戳
# server_name 服务器别名
def to_dingding_msg(self, push_data: dict, push_public_data: dict) -> str:
msg_list = push_data.get('msg_list', None)
if msg_list is None:
raise ValueError("Task: {} alert push data parameter error, there is no msg_list field".format(self.title))
return self.public_headers_msg(push_public_data,dingding=True) + "\n\n" + "\n\n".join(msg_list)
def to_feishu_msg(self, push_data: dict, push_public_data: dict) -> str:
msg_list = push_data.get('msg_list', None)
if msg_list is None:
raise ValueError("Task: {} alert push data parameter error, there is no msg_list field".format(self.title))
return self.public_headers_msg(push_public_data) + "\n\n" + "\n\n".join(msg_list)
def to_mail_msg(self, push_data: dict, push_public_data: dict) -> str:
msg_list = push_data.get('msg_list', None)
if msg_list is None:
raise ValueError("Task: {} alert push data parameter error, there is no msg_list field".format(self.title))
public_headers = self.public_headers_msg(push_public_data, "<br>")
return public_headers + "<br>" + "<br>".join(msg_list)
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
"""
返回 短信告警的类型和数据
@param push_data:
@param push_public_data:
@return: 第一项是类型, 第二项是数据
"""
raise NotImplementedError()
def to_tg_msg(self, push_data: dict, push_public_data: dict) -> str:
msg_list = push_data.get('msg_list', None)
if msg_list is None:
raise ValueError("Task: {} alert push data parameter error, there is no msg_list field".format(self.title))
public_headers = self.public_headers_msg(push_public_data, "<br>")
return public_headers + "<br>" + "<br>".join(msg_list)
def to_weixin_msg(self, push_data: dict, push_public_data: dict) -> str:
msg_list = push_data.get('msg_list', None)
if msg_list is None:
raise ValueError("Task: {} alert push data parameter error, there is no msg_list field".format(self.title))
spc = "\n "
public_headers = self.public_headers_msg(push_public_data, "\n ")
return public_headers + spc + spc.join(msg_list)
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
raise NotImplementedError()
def to_web_hook_msg(self, push_data: dict, push_public_data: dict) -> str:
msg_list = push_data.get('msg_list', None)
if msg_list is None:
raise ValueError("Task: {} alert push data parameter error, there is no msg_list field".format(self.title))
public_headers = self.public_headers_msg(push_public_data, "\n")
return public_headers + "\n" + "\n".join(msg_list)
def public_headers_msg(self, push_public_data: dict, spc: str = None,dingding=False) -> str:
if spc is None:
spc = "\n\n"
title = self.title
print(title)
if dingding:
print("dingdingtitle",title)
if "yakpanel" not in title:
title += "yakpanel"
print("dingdingtitle",title)
print(title)
return spc.join([
"#### {}".format(title),
">Server:" + push_public_data['server_name'],
">IPAddress: {}(Internet) {}(Internal)".format(push_public_data['ip'], push_public_data['local_ip']),
">SendingTime: " + push_public_data['time']
])
class BaseTaskViewMsg:
def get_msg(self, task: dict) -> Optional[str]:
return ""

View File

@@ -0,0 +1,27 @@
import os
from .util import read_file, write_file
def rsync_compatible():
files = [
"/www/server/panel/class/push/rsync_push.py",
"/www/server/panel/plugin/rsync/rsync_push.py",
]
for f in files:
print(f)
if not os.path.exists(f):
continue
src_data = read_file(f)
if src_data.find("push_rsync_by_task_name") != -1:
continue
src_data = src_data.replace("""if __name__ == "__main__":
rsync_push().main()""", """
if __name__ == "__main__":
try:
sys.path.insert(0, "/www/server/panel")
from mod.base.push_mod.rsync_push import push_rsync_by_task_name
push_rsync_by_task_name(sys.argv[1])
except:
rsync_push().main()
""")
write_file(f, src_data)

View File

@@ -0,0 +1,239 @@
import json
import os
import sys
import ipaddress
from datetime import datetime, timedelta
from typing import Tuple, Union, Optional
from .send_tool import WxAccountMsg
from .base_task import BaseTask
from .util import read_file, DB, GET_CLASS
try:
if "/www/server/panel/class" not in sys.path:
sys.path.insert(0, "/www/server/panel/class")
from panel_msg.collector import DatabasePushMsgCollect
except ImportError:
DatabasePushMsgCollect = None
def is_ipaddress(ip_data: str) -> bool:
try:
ipaddress.ip_address(ip_data)
except ValueError:
return False
return True
# class MysqlPwdEndTimeTask(BaseTask):
#
# def __init__(self):
# super().__init__()
# self.template_name = "MySQL数据库密码到期"
# self.source_name = "mysql_pwd_end"
#
# self.push_db_user = ""
#
# def get_title(self, task_data: dict) -> str:
# return "Msql:" + task_data["project"][1] + "用户密码到期提醒"
#
# def check_task_data(self, task_data: dict) -> Union[dict, str]:
# task_data["interval"] = 600
# if not (isinstance(task_data["project"], list) and len(task_data["project"]) == 3):
# return "设置的用户格式错误"
# project = task_data["project"]
# if not (isinstance(project[0], int) and isinstance(project[1], str) and is_ipaddress(project[2])):
# return "设置的检测用户格式错误"
#
# if not (isinstance(task_data["cycle"], int) and task_data["cycle"] >= 1):
# return "到期时间参数错误,至少为 1 天"
# return task_data
#
# def get_keyword(self, task_data: dict) -> str:
# return "_".join([str(i) for i in task_data["project"]])
#
# def check_num_rule(self, num_rule: dict) -> Union[dict, str]:
# num_rule["day_num"] = 1
# return num_rule
#
# def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
# sid = task_data["project"][0]
# username = task_data["project"][1]
# host = task_data["project"][2]
#
# if "/www/server/panel/class" not in sys.path:
# sys.path.insert(0, "/www/server/panel/class")
# try:
# import panelMysql
# import db_mysql
# except ImportError:
# return None
#
# if sid == 0:
# try:
# db_port = int(panelMysql.panelMysql().query("show global variables like 'port'")[0][1])
# if db_port == 0:
# db_port = 3306
# except:
# db_port = 3306
# conn_config = {
# "db_host": "localhost",
# "db_port": db_port,
# "db_user": "root",
# "db_password": DB("config").where("id=?", (1,)).getField("mysql_root"),
# "ps": "local server",
# }
# else:
# conn_config = DB("database_servers").where("id=? AND LOWER(db_type)=LOWER('mysql')", (sid,)).find()
# if not conn_config:
# return None
#
# mysql_obj = db_mysql.panelMysql().set_host(conn_config["db_host"], conn_config["db_port"], None,
# conn_config["db_user"], conn_config["db_password"])
# if isinstance(mysql_obj, bool):
# return None
#
# data_list = mysql_obj.query(
# "SELECT password_last_changed FROM mysql.user WHERE user='{}' AND host='{}';".format(username, host))
#
# if not isinstance(data_list, list) or not data_list:
# return None
#
# try:
# # todo检查这里的时间转化逻辑问题
# last_time = data_list[0][0]
# expire_time = last_time + timedelta(days=task_data["cycle"])
# except:
# return None
#
# if datetime.now() > expire_time:
# self.title = self.get_title(task_data)
# self.push_db_user = username
# return {"msg_list": [
# ">告警类型MySQL密码即将到期",
# ">Content of alarm: {} {}@{} 密码过期时间<font color=#ff0000>{} 天</font>".format(
# conn_config["ps"], username, host, expire_time.strftime("%Y-%m-%d %H:%M:%S"))
# ]}
#
# def filter_template(self, template: dict) -> Optional[dict]:
# return template
#
# def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
# return "", {}
#
# def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
# msg = WxAccountMsg.new_msg()
# msg.thing_type = "MySQL数据库密码到期"
# msg.msg = "Mysql用户{}的密码即将过期,请注意".format(self.push_db_user)
# msg.next_msg = "Please log in to the panel to view the host status"
# return msg
class MysqlReplicateStatusTask(BaseTask):
def __init__(self):
super().__init__()
self.template_name = "MySQL主从复制异常告警"
self.source_name = "mysql_replicate_status"
self.title = "MySQL主从复制异常告警"
self.slave_ip = ''
def check_task_data(self, task_data: dict) -> Union[dict, str]:
if not (isinstance(task_data["project"], str) and task_data["project"]):
return "请选择告警的从库!"
if not (isinstance(task_data["count"], int) and task_data["count"] in (1, 2)):
return "是否自动修复选择错误!"
if not (isinstance(task_data["interval"], int) and task_data["interval"] >= 60):
return "检查间隔时间错误至少需要60s的间隔"
return task_data
def get_keyword(self, task_data: dict) -> str:
return task_data["project"]
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
import PluginLoader
args = GET_CLASS()
args.slave_ip = task_data["project"]
res = PluginLoader.plugin_run("mysql_replicate", "get_replicate_status", args)
if res.get("status", False) is False:
return None
self.slave_ip = task_data["project"]
if len(res.get("data", [])) == 0:
s_list = [">告警类型MySQL主从复制异常告警",
">Content of alarm: <font color=#ff0000>从库 {} 主从复制已停止,请尽快登录面板查看详情</font>".format(
task_data["project"])]
return {"msg_list": s_list}
sql_status = io_status = False
for item in res.get("data", []):
if item["name"] == "Slave_IO_Running" and item["value"] == "Yes":
io_status = True
if item["name"] == "Slave_SQL_Running" and item["value"] == "Yes":
sql_status = True
if io_status is True and sql_status is True:
break
if io_status is False or sql_status is False:
repair_txt = "请尽快登录面板查看详情"
if task_data["count"] == 1: # 自动修复
PluginLoader.plugin_run("mysql_replicate", "repair_replicate", args)
repair_txt = ",正在尝试修复"
s_list = [">告警类型MySQL主从复制异常告警",
">Content of alarm: <font color=#ff0000>从库 {} 主从复制发生异常{}</font>".format(
task_data["project"], repair_txt)]
return {"msg_list": s_list}
return None
@staticmethod
def _get_mysql_replicate():
slave_list = []
mysql_replicate_path = os.path.join("/www/server/panel/plugin", "mysql_replicate", "config.json")
if os.path.isfile(mysql_replicate_path):
conf = read_file(mysql_replicate_path)
try:
conf = json.loads(conf)
slave_list = [{"title": slave_ip, "value": slave_ip} for slave_ip in conf["slave"].keys()]
except:
pass
return slave_list
def filter_template(self, template: dict) -> Optional[dict]:
template["field"][0]["items"] = self._get_mysql_replicate()
if not template["field"][0]["items"]:
return None
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return '', {}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "MySQL主从复制异常告警"
msg.msg = "从库 {} 主从复制发生异常".format(self.slave_ip)
msg.next_msg = "请登录面板,在[软件商店-MySQL主从复制(重构版)]中查看"
return msg
class ViewMsgFormat(object):
_FORMAT = {
"30": (
lambda x: "<span>剩余时间小于{}{}</span>".format(
x["task_data"].get("cycle"),
("(如未处理次日会重新发送1次持续%d天)" % x.get("number_rule", {}).get("day_num", 0)) if x.get(
"number_rule", {}).get("day_num", 0) else ""
)
),
"31": (lambda x: "<span>MySQL主从复制异常告警</span>".format()),
}
def get_msg(self, task: dict) -> Optional[str]:
if task["template_id"] in self._FORMAT:
return self._FORMAT[task["template_id"]](task)
return None

View File

@@ -0,0 +1,81 @@
[
{
"id": "31",
"ver": "1",
"used": true,
"source": "mysql_replicate_status",
"title": "Msql主从同步告警",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.database_push",
"name": "MysqlReplicateStatusTask"
},
"template": {
"field": [
{
"attr": "project",
"name": "选择监控的从库",
"type": "select",
"default": null,
"items": []
},
{
"attr": "count",
"name": "自动修复",
"type": "radio",
"suffix": "",
"default": 1,
"items": [
{
"title": "自动尝试修复",
"value": 1
},
{
"title": "不做修复尝试",
"value": 2
}
]
},
{
"attr": "interval",
"name": "间隔时间",
"type": "number",
"unit": "秒",
"suffix": "后再次监控检测条件",
"default": 600
}
],
"sorted": [
[
"project"
],
[
"count"
],
[
"interval"
]
]
},
"default": {
"project": "",
"count": 2,
"interval": 600
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"wx_account",
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": false
}
]

View File

@@ -0,0 +1,178 @@
import glob
import hashlib
import json
import os
import re
import sys
import time
import psutil
from datetime import datetime
from importlib import import_module
from typing import Tuple, Union, Optional, List
from .send_tool import WxAccountMsg, WxAccountLoginMsg
from .base_task import BaseTask
from .mods import PUSH_DATA_PATH, TaskConfig, SenderConfig, PANEL_PATH
from .util import read_file, DB, write_file, check_site_status,GET_CLASS, ExecShell, get_config_value, public_get_cache_func, \
public_set_cache_func, get_network_ip, public_get_user_info, public_http_post, panel_version
from mod.base.web_conf import RealSSLManger
# 邮局域名进入黑名单
class MailDomainBlcheck(BaseTask):
push_tip_file = "/www/server/panel/data/mail_domain_blcheck_send_type.pl"
def __init__(self):
super().__init__()
self.source_name = "mail_domain_black"
self.template_name = "Your IP is on the email blacklist"
self.title = "Your IP is on the email blacklist"
def check_task_data(self, task_data: dict) -> Union[dict, str]:
return {}
def get_keyword(self, task_data: dict) -> str:
return "mail_domain_black"
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
return None
def filter_template(self, template) -> dict:
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return "", {}
def task_config_update_hook(self, task: dict) -> None:
sender = task["sender"]
if len(sender) > 0:
send_id = sender[0]
else:
return
sender_data = SenderConfig().get_by_id(send_id)
if sender_data:
write_file(self.push_tip_file, sender_data["sender_type"])
def task_config_create_hook(self, task: dict) -> None:
return self.task_config_update_hook(task)
def task_config_remove_hook(self, task: dict) -> None:
if os.path.exists(self.push_tip_file):
os.remove(self.push_tip_file)
# 邮局服务异常告警
class MailServerDown(BaseTask):
push_tip_file = "/www/server/panel/data/mail_server_down_send_type.pl"
def __init__(self):
super().__init__()
self.source_name = "mail_server_status"
self.template_name = "Your Mail Service is down"
self.title = "Your Mail Service is down"
def check_task_data(self, task_data: dict) -> Union[dict, str]:
return {}
def get_keyword(self, task_data: dict) -> str:
return "mail_server_status"
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
return None
def filter_template(self, template) -> dict:
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return "", {}
def task_config_update_hook(self, task: dict) -> None:
sender = task["sender"]
if len(sender) > 0:
send_id = sender[0]
else:
return
sender_data = SenderConfig().get_by_id(send_id)
if sender_data:
write_file(self.push_tip_file, sender_data["sender_type"])
def task_config_create_hook(self, task: dict) -> None:
return self.task_config_update_hook(task)
def task_config_remove_hook(self, task: dict) -> None:
if os.path.exists(self.push_tip_file):
os.remove(self.push_tip_file)
# 邮局服务异常告警
class MailDomainQuota(BaseTask):
push_tip_file = "/www/server/panel/data/mail_domain_quota_alert_send_type.pl"
def __init__(self):
super().__init__()
self.source_name = "mail_domain_quota_alert"
self.template_name = "Your Mail Domain Quota Alert"
self.title = "Your Mail Domain Quota Alert"
def check_task_data(self, task_data: dict) -> Union[dict, str]:
return {}
def get_keyword(self, task_data: dict) -> str:
return "mail_domain_quota_alert"
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
return None
def filter_template(self, template) -> dict:
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return "", {}
def task_config_update_hook(self, task: dict) -> None:
sender = task["sender"]
if len(sender) > 0:
send_id = sender[0]
else:
return
sender_data = SenderConfig().get_by_id(send_id)
if sender_data:
write_file(self.push_tip_file, sender_data["sender_type"])
def task_config_create_hook(self, task: dict) -> None:
return self.task_config_update_hook(task)
def task_config_remove_hook(self, task: dict) -> None:
if os.path.exists(self.push_tip_file):
os.remove(self.push_tip_file)
class ViewMsgFormat(object):
_FORMAT = {
"1": (
lambda x: "<span>When your MailServer domain is blacklisted, an alarm is generated</span>"
),
"2": (
lambda x: "<span>When your Mail Service is down, an alarm is generated</span>"
),
"3": (
lambda x: "<span>When your Mail domain usage exceeds quota, an alarm is generated</span>"
)
}
def get_msg(self, task: dict) -> Optional[str]:
if task["template_id"] in ["80"]:
return self._FORMAT["1"](task)
if task["template_id"] in ["81"]:
return self._FORMAT["2"](task)
if task["template_id"] in ["82"]:
return self._FORMAT["3"](task)
if task["template_id"] in self._FORMAT:
return self._FORMAT[task["template_id"]](task)
return None

View File

@@ -0,0 +1,105 @@
[
{
"id": "80",
"ver": "1",
"used": true,
"source": "mail_domain_black",
"title": "Your IP is on the email blacklist",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.domain_blcheck_push",
"name": "MailDomainBlcheck"
},
"template": {
"field": [
],
"sorted": [
]
},
"default": {
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": true
},
{
"id": "81",
"ver": "1",
"used": true,
"source": "mail_server_status",
"title": "Your Mail Service is down",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.domain_blcheck_push",
"name": "MailServerDown"
},
"template": {
"field": [
],
"sorted": [
]
},
"default": {
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": true
},
{
"id": "82",
"ver": "1",
"used": true,
"source": "mail_domain_quota_alert",
"title": "Your Mail Domain Quota Alert",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.domain_blcheck_push",
"name": "MailDomainQuota"
},
"template": {
"field": [
],
"sorted": [
]
},
"default": {
},
"advanced_default": {
"number_rule": {
"day_num": 1
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": true
}
]

View File

@@ -0,0 +1,274 @@
import json
import os
import time
from typing import Tuple, Union, Optional
from .mods import PUSH_DATA_PATH, TaskTemplateConfig
from .send_tool import WxAccountMsg
from .base_task import BaseTask
from .util import read_file, DB, GET_CLASS, write_file
class NginxLoadTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "nginx_load_push"
self.template_name = "Load balancing alarm"
# self.title = "Load balancing alarm"
self._tip_counter = None
@property
def tip_counter(self) -> dict:
if self._tip_counter is not None:
return self._tip_counter
tip_counter = '{}/load_balance_push.json'.format(PUSH_DATA_PATH)
if os.path.exists(tip_counter):
try:
self._tip_counter = json.loads(read_file(tip_counter))
except json.JSONDecodeError:
self._tip_counter = {}
else:
self._tip_counter = {}
return self._tip_counter
def save_tip_counter(self):
tip_counter = '{}/load_balance_push.json'.format(PUSH_DATA_PATH)
write_file(tip_counter, json.dumps(self.tip_counter))
def get_title(self, task_data: dict) -> str:
if task_data["project"] == "all":
return "Load balancing alarm"
return "Load balancing alarm -- [{}] ".format(task_data["project"])
def check_task_data(self, task_data: dict) -> Union[dict, str]:
all_upstream_name = DB("upstream").field("name").select()
if isinstance(all_upstream_name, str) and all_upstream_name.startswith("error"):
return 'Alarms cannot be set without load balancing configuration'
all_upstream_name = [i["name"] for i in all_upstream_name]
if not bool(all_upstream_name):
return 'Alarms cannot be set without load balancing configuration'
if task_data["project"] not in all_upstream_name and task_data["project"] != "all":
return 'Without this load balancer configuration, alarms cannot be set'
cycle = []
for i in task_data["cycle"].split("|"):
if bool(i) and i.isdecimal():
code = int(i)
if 100 <= code < 600:
cycle.append(str(code))
if not bool(cycle):
return 'If no error code is specified, the alarm cannot be set'
task_data["cycle"] = "|".join(cycle)
return task_data
def get_keyword(self, task_data: dict) -> str:
return task_data["project"]
def _check_func(self, upstream_name: str, codes: str) -> list:
import PluginLoader
get_obj = GET_CLASS()
get_obj.upstream_name = upstream_name
# 调用外部插件检查负载均衡的健康状况
upstreams = PluginLoader.plugin_run("load_balance", "get_check_upstream", get_obj)
access_codes = [int(i) for i in codes.split("|") if bool(i.strip())]
res_list = []
for upstream in upstreams:
# 检查每个节点,返回有问题的节点信息
res = upstream.check_nodes(access_codes, return_nodes=True)
for ping_url in res:
if ping_url in self.tip_counter:
self.tip_counter[ping_url].append(int(time.time()))
idx = 0
for i in self.tip_counter[ping_url]:
# 清理超过4分钟的记录
if time.time() - i > 60 * 4:
idx += 1
self.tip_counter[ping_url] = self.tip_counter[ping_url][idx:]
print("self.tip_counter[ping_url]",self.tip_counter[ping_url])
# 如果一个节点连续三次出现在告警列表中,则视为需要告警
if len(self.tip_counter[ping_url]) >= 3:
res_list.append(ping_url)
self.tip_counter[ping_url] = []
else:
self.tip_counter[ping_url] = [int(time.time()), ]
self.save_tip_counter()
return res_list
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
err_nodes = self._check_func(task_data["project"], task_data["cycle"])
if not err_nodes:
return None
pj = "load balancing:【{}".format(task_data["project"]) if task_data["project"] != "all" else "load balancing"
nodes = ''.join(err_nodes)
self.title = self.get_title(task_data)
return {
"msg_list": [
">Notification type: Enterprise Edition load balancing alarm",
">Content of alarm: <font color=#ff0000>{}The node [{}] under the configuration has access error, please pay attention to the node situation in time and deal with it.</font> ".format(
pj, nodes),
],
"pj": pj,
"nodes": nodes
}
def filter_template(self, template: dict) -> Optional[dict]:
if not os.path.exists("/www/server/panel/plugin/load_balance/load_balance_main.py"):
return None
all_upstream = DB("upstream").field("name").select()
if isinstance(all_upstream, str) and all_upstream.startswith("error"):
return None
all_upstream_name = [i["name"] for i in all_upstream]
if not all_upstream_name:
return None
for name in all_upstream_name:
template["field"][0]["items"].append({
"title": name,
"value": name
})
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return '', {}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "Load balancing alarm"
msg.msg = "If the node is abnormal, log in to the panel"
return msg
def task_config_create_hook(self, task: dict) -> None:
old_config_file = "/www/server/panel/class/push/push.json"
try:
old_config = json.loads(read_file(old_config_file))
except:
return
if "load_balance_push" not in old_config:
old_config["load_balance_push"] = {}
old_data = {
"push_count": task["number_rule"].get("day_num", 2),
"cycle": task["task_data"].get("cycle", "200|301|302|403|404"),
"interval": task["task_data"].get("interval", 60),
"title": task["title"],
"status": task['status'],
"module": ",".join(task["sender"])
}
for k, v in old_config["load_balance_push"].items():
if v["project"] == task["task_data"]["project"]:
v.update(old_data)
else:
old_data["project"] = task["task_data"]["project"]
old_config["load_balance_push"][int(time.time())] = old_data
write_file(old_config_file, json.dumps(old_config))
def task_config_update_hook(self, task: dict) -> None:
return self.task_config_create_hook(task)
def task_config_remove_hook(self, task: dict) -> None:
old_config_file = "/www/server/panel/class/push/push.json"
try:
old_config = json.loads(read_file(old_config_file))
except:
return
if "load_balance_push" not in old_config:
old_config["load_balance_push"] = {}
old_config["load_balance_push"] = {
k: v for k, v in old_config["load_balance_push"].items()
if v["project"] != task["task_data"]["project"]
}
def load_load_template():
if TaskTemplateConfig().get_by_id("50"):
return None
from .mods import load_task_template_by_config
load_task_template_by_config(
[{
"id": "50",
"ver": "1",
"used": True,
"source": "nginx_load_push",
"title": "load balancing",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.load_push",
"name": "NginxLoadTask"
},
"template": {
"field": [
{
"attr": "project",
"name": "The name of the payload",
"type": "select",
"default": "all",
"unit": "",
"suffix": (
"<i style='color: #999;font-style: initial;font-size: 12px;margin-right: 5px'>*</i>"
"<span style='color:#999'>If a node fails to access a node in the selected load configuration, an alarm is triggered</span>"
),
"items": [
{
"title": "All configured loads",
"value": "all"
}
]
},
{
"attr": "cycle",
"name": "The status code of the success",
"type": "textarea",
"unit": "",
"suffix": (
"<br><i style='color: #999;font-style: initial;font-size: 12px;margin-right: 5px'>*</i>"
"<span style='color:#999'>Status codes are separated by vertical bars, for example:200|301|302|403|404</span>"
),
"width": "400px",
"style": {
'height': '70px',
},
"default": "200|301|302|403|404"
}
],
"sorted": [
[
"project"
],
[
"cycle"
]
],
},
"default": {
"project": "all",
"cycle": "200|301|302|403|404"
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"wx_account",
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg",
],
"unique": False
}]
)
class ViewMsgFormat(object):
@staticmethod
def get_msg(task: dict) -> Optional[str]:
if task["template_id"] == "50":
return "<span>When the node access is abnormal, the alarm message is pushed (it is not pushed after {} times per day)<span>".format(
task.get("number_rule", {}).get("day_num"))
return None

View File

@@ -0,0 +1,296 @@
import json
import os
import sys
import time
from typing import Union, Optional
from mod.base import json_response
from .mods import TaskTemplateConfig, TaskConfig, SenderConfig, TaskRecordConfig
from .system import PushSystem
sys.path.insert(0, "/www/server/panel/class/")
import public
class PushManager:
def __init__(self):
self.template_conf = TaskTemplateConfig()
self.task_conf = TaskConfig()
self.send_config = SenderConfig()
self._send_conf_cache = {}
def _get_sender_conf(self, sender_id):
if sender_id in self._send_conf_cache:
return self._send_conf_cache[sender_id]
tmp = self.send_config.get_by_id(sender_id)
self._send_conf_cache[sender_id] = tmp
return tmp
def normalize_task_config(self, task, template) -> Union[dict, str]:
result = {}
sender = task.get("sender", None)
if sender is None:
return "No alarm channel is configured"
if not isinstance(sender, list):
return "The alarm channel is incorrect"
new_sender = []
for i in sender:
sender_conf = self._get_sender_conf(i)
if not sender_conf:
continue
else:
new_sender.append(i)
if sender_conf["sender_type"] not in template["send_type_list"]:
if sender_conf["sender_type"] == "sms":
return "SMS alerts are not supported"
return "Unsupported alerting methods:{}".format(sender_conf['data']["title"])
if not sender_conf["used"]:
if sender_conf["sender_type"] == "sms":
return "The SMS alert channel has been closed"
return "Closed alert mode:{}".format(sender_conf['data']["title"])
result["sender"] = new_sender
if "default" in template and template["default"]:
task_data = task.get("task_data", {})
for k, v in template["default"].items():
if k not in task_data:
task_data[k] = v
result["task_data"] = task_data
# 避免default为空时无数据
else:
result["task_data"] = task.get("task_data", {})
if "task_data" not in result:
result["task_data"] = {}
time_rule = task.get("time_rule", {})
if "send_interval" in time_rule:
if not isinstance(time_rule["send_interval"], int):
return "The minimum interval is set incorrectly"
if time_rule["send_interval"] < 0:
return "The minimum interval is set incorrectly"
if "time_range" in time_rule:
if not isinstance(time_rule["time_range"], list):
return "The time range is set incorrectly"
if not len(time_rule["time_range"]) == 2:
del time_rule["time_range"]
else:
time_range = time_rule["time_range"]
if not (isinstance(time_range[0], int) and isinstance(time_range[1], int) and
0 <= time_range[0] < time_range[1] <= 60 * 60 * 24):
return "The time range is set incorrectly"
result["time_rule"] = time_rule
number_rule = task.get("number_rule", {})
if "day_num" in number_rule:
if not (isinstance(number_rule["day_num"], int) and number_rule["day_num"] >= 0):
return "The minimum number of times per day is set incorrectly"
if "total" in number_rule:
if not (isinstance(number_rule["total"], int) and number_rule["total"] >= 0):
return "The maximum number of alarms is set incorrectly"
result["number_rule"] = number_rule
if "status" not in task:
result["status"] = True
if "status" in task:
if isinstance(task["status"], bool):
result["status"] = task["status"]
return result
def set_task_conf_data(self, push_data: dict) -> Optional[str]:
task_id = push_data.get("task_id", None)
template_id = push_data.get("template_id")
task = push_data.get("task_data")
target_task_conf = None
if task_id is not None:
tmp = self.task_conf.get_by_id(task_id)
if tmp is None:
target_task_conf = tmp
template = self.template_conf.get_by_id(template_id)
if not template:
# 如果没有找到模板,则尝试加载默认的安全推送模板
from .mods import load_task_template_by_file
if not os.path.exists("/www/server/panel/mod/base/push_mod/safe_mod_push_template.json"):
return "No alarm template was found"
load_task_template_by_file("/www/server/panel/mod/base/push_mod/safe_mod_push_template.json")
self.template_conf = TaskTemplateConfig()
template = self.template_conf.get_by_id(template_id)
if not template:
return "No alarm template was found"
if template["unique"] and not target_task_conf:
for i in self.task_conf.config:
if i["template_id"] == template["id"]:
target_task_conf = i
break
task_obj = PushSystem().get_task_object(template_id, template["load_cls"])
if not task_obj:
return "Loading task type error, you can try to fix the panel"
res = self.normalize_task_config(task, template)
if isinstance(res, str):
return res
task_data = task_obj.check_task_data(res["task_data"])
if isinstance(task_data, str):
return task_data
number_rule = task_obj.check_num_rule(res["number_rule"])
if isinstance(number_rule, str):
return number_rule
time_rule = task_obj.check_time_rule(res["time_rule"])
if isinstance(time_rule, str):
return time_rule
res["task_data"] = task_data
res["number_rule"] = number_rule
res["time_rule"] = time_rule
res["keyword"] = task_obj.get_keyword(task_data)
res["source"] = task_obj.source_name
res["title"] = task_obj.get_title(task_data)
if not target_task_conf:
tmp = self.task_conf.get_by_keyword(res["source"], res["keyword"])
if tmp:
target_task_conf = tmp
if not target_task_conf:
res["id"] = self.task_conf.nwe_id()
res["template_id"] = template_id
res["status"] = True
res["pre_hook"] = {}
res["after_hook"] = {}
res["last_check"] = 0
res["last_send"] = 0
res["number_data"] = {}
res["create_time"] = time.time()
res["record_time"] = 0
self.task_conf.config.append(res)
task_obj.task_config_create_hook(res)
else:
target_task_conf.update(res)
target_task_conf["last_check"] = 0
target_task_conf["number_data"] = {} # 次数控制数据置空
task_obj.task_config_update_hook(target_task_conf)
self.task_conf.save_config()
return None
def update_task_status(self, get):
# 先调用 set_task_conf 修改任务配置
set_conf_response = self.set_task_conf(get)
if set_conf_response['status'] != 0:
return set_conf_response # 返回错误信息
# 读取任务数据
file_path = '{}/data/mod_push_data/task.json'.format(public.get_panel_path())
try:
with open(file_path, 'r') as file:
tasks = json.load(file)
except (IOError, json.JSONDecodeError):
return json_response(status=False, msg=public.lang("Unable to read task data."))
# 查找对应的 task_id
task_title = get.title.strip() # 假设 get 中有 title 参数
task_id = None
for task in tasks:
if task.get('title') == task_title:
task_id = task.get('id')
break
if not task_id:
return json_response(status=False, msg=public.lang("The task has not been found."))
# 调用 change_task_conf 修改任务状态
get.task_id = task_id
return self.change_task_conf(get)
def set_task_conf(self, get):
task_id = None
try:
if hasattr(get, "task_id"):
task_id = get.task_id.strip()
if not task_id:
task_id = None
else:
self.remove_task_conf(get)
template_id = get.template_id.strip()
task = json.loads(get.task_data.strip())
except (AttributeError, json.JSONDecodeError, TypeError, ValueError):
return json_response(status=False, msg="The parameter is incorrect")
push_data = {
"task_id": task_id,
"template_id": template_id,
"task_data": task,
}
res = self.set_task_conf_data(push_data)
if res:
return json_response(status=False, msg=res)
return json_response(status=True, msg="The alarm task is saved successfully")
def change_task_conf(self, get):
try:
task_id = get.task_id.strip()
status = int(get.status) # 获取status字段并转换为整数
except (AttributeError, ValueError):
return json_response(status=False, msg="Parameter error")
if status not in [0, 1]:
return json_response(status=False, msg="Invalid status value")
tmp = self.task_conf.get_by_id(task_id)
if tmp is None:
return json_response(status=True, msg="No alarm task was queried")
tmp["status"] = bool(status) # 将status转换为布尔值并设置
self.task_conf.save_config()
return json_response(status=True, msg="operate successfully")
def change_task(self, task_id, status):
tmp = self.task_conf.get_by_id(task_id)
tmp["status"] = bool(status) # 将status转换为布尔值并设置
self.task_conf.save_config()
def remove_task_conf(self, get):
try:
task_id = get.task_id.strip()
except AttributeError:
return json_response(status=False, msg="The parameter is incorrect")
tmp = self.task_conf.get_by_id(task_id)
if tmp is None:
return json_response(status=True, msg="No alarm task was queried")
self.task_conf.config.remove(tmp)
self.task_conf.save_config()
template = self.template_conf.get_by_id(tmp["template_id"])
if template:
task_obj = PushSystem().get_task_object(template["id"], template["load_cls"])
if task_obj:
task_obj.task_config_remove_hook(tmp)
return json_response(status=True, msg="operate successfully")
@staticmethod
def clear_task_record_by_task_id(task_id):
tr_conf = TaskRecordConfig(task_id)
if os.path.exists(tr_conf.config_file_path):
os.remove(tr_conf.config_file_path)

371
mod/base/push_mod/mods.py Normal file
View File

@@ -0,0 +1,371 @@
# coding: utf-8
# -------------------------------------------------------------------
# yakpanel
# -------------------------------------------------------------------
# Copyright (c) 2015-2017 yakpanel(https://www.yakpanel.com) All rights reserved.
# -------------------------------------------------------------------
# Author: baozi <baozi@yakpanel.com>
# -------------------------------------------------------------------
# 新告警的所有数据库操作
# ------------------------------
import json
import os
import types
from typing import Any, Dict, Optional, List
from threading import Lock
from uuid import uuid4
import fcntl
from .util import Sqlite, write_log, read_file, write_file
_push_db_lock = Lock()
# 代替 class/db.py 中, 离谱的两个query函数
def msg_db_query_func(self, sql, param=()):
# 执行SQL语句返回数据集
self._Sql__GetConn()
try:
return self._Sql__DB_CONN.execute(sql, self._Sql__to_tuple(param))
except Exception as ex:
return "error: " + str(ex)
def get_push_db():
db_file = "/www/server/panel/data/db/mod_push.db"
if not os.path.isdir(os.path.dirname(db_file)):
os.makedirs(os.path.dirname(db_file))
db = Sqlite()
setattr(db, "_Sql__DB_FILE", db_file)
setattr(db, "query", types.MethodType(msg_db_query_func, db))
return db
def get_table(table_name: str):
db = get_push_db()
db.table = table_name
return db
def lock_push_db():
with open("/www/server/panel/data/db/mod_push.db", mode="rb") as msg_fd:
fcntl.flock(msg_fd.fileno(), fcntl.LOCK_EX)
_push_db_lock.locked()
def unlock_push_db():
with open("/www/server/panel/data/db/mod_push.db", mode="rb") as msg_fd:
fcntl.flock(msg_fd.fileno(), fcntl.LOCK_UN)
_push_db_lock.acquire()
def push_db_locker(func):
def inner_func(*args, **kwargs):
lock_push_db()
try:
res = func(*args, **kwargs)
except Exception as e:
unlock_push_db() # 即使 报错了 也先解锁再操作
raise e
else:
unlock_push_db()
return res
return inner_func
DB_INIT_ERROR = False
PANEL_PATH = "/www/server/panel"
PUSH_DATA_PATH = "{}/data/mod_push_data".format(PANEL_PATH)
UPDATE_VERSION_FILE = "{}/update_panel.pl".format(PUSH_DATA_PATH)
UPDATE_MOD_PUSH_FILE = "{}/update_mod.pl".format(PUSH_DATA_PATH)
class BaseConfig:
config_file_path = ""
# config_file_path = "/www/server/panel/data/mod_push_data/task.json"
# /www/server/panel/data/mod_push_data/sender.json
def __init__(self):
if not os.path.exists(PUSH_DATA_PATH):
os.makedirs(PUSH_DATA_PATH)
self._config: Optional[List[Dict[str, Any]]] = None
@property
def config(self) -> List[Dict[str, Any]]:
if self._config is None:
try:
self._config = json.loads(read_file(self.config_file_path))
except:
self._config = []
return self._config
def save_config(self) -> None:
write_file(self.config_file_path, json.dumps(self.config))
@staticmethod
def nwe_id() -> str:
return uuid4().hex[::2]
def get_by_id(self, target_id: str) -> Optional[Dict[str, Any]]:
for i in self.config:
if i.get("id", None) == target_id:
return i
class TaskTemplateConfig(BaseConfig):
config_file_path = "{}/task_template.json".format(PUSH_DATA_PATH)
class TaskConfig(BaseConfig):
config_file_path = "{}/task.json".format(PUSH_DATA_PATH)
def get_by_keyword(self, source: str, keyword: str) -> Optional[Dict[str, Any]]:
for i in self.config:
if i.get("source", None) == source and i.get("keyword", None) == keyword:
return i
class TaskRecordConfig(BaseConfig):
config_file_path_fmt = "%s/task_record_{}.json" % PUSH_DATA_PATH
def __init__(self, task_id: str):
super().__init__()
self.config_file_path = self.config_file_path_fmt.format(task_id)
class SenderConfig(BaseConfig):
config_file_path = "{}/sender.json".format(PUSH_DATA_PATH)
def __init__(self):
super(SenderConfig, self).__init__()
if not os.path.exists(self.config_file_path):
write_file(self.config_file_path, json.dumps([{
"id": self.nwe_id(),
"used": True,
"sender_type": "sms",
"data": {},
"original": True
}]))
def init_db():
global DB_INIT_ERROR
# id 模板id 必须唯一, 后端开发需要协商
# ver 模板版本号, 用于更新
# used 是否在使用用
# source 来源, 如Waf, rsync
# title 标题
# load_cls 要加载的类,或者从那种调用方法中获取到任务处理对象
# template 给前端,用于展示的数据
# default 默认数据,用于数据过滤, 和默认值
# unique 是否仅可唯一设置
# create_time 创建时间
create_task_template_sql = (
"CREATE TABLE IF NOT EXISTS 'task_template' ("
"'id' INTEGER PRIMARY KEY AUTOINCREMENT, "
"'ver' TEXT NOT NULL DEFAULT '1.0.0', "
"'used' INTEGER NOT NULL DEFAULT 1, "
"'source' TEXT NOT NULL DEFAULT 'site_push', "
"'title' TEXT NOT NULL DEFAULT '', "
"'load_cls' TEXT NOT NULL DEFAULT '{}', "
"'template' TEXT NOT NULL DEFAULT '{}', "
"'default' TEXT NOT NULL DEFAULT '{}', "
"'send_type_list' TEXT NOT NULL DEFAULT '[]', "
"'unique' INTEGER NOT NULL DEFAULT 0, "
"'create_time' INTEGER NOT NULL DEFAULT (strftime('%s'))"
");"
)
# source 来源, 例如waf(防火墙), rsync(文件同步)
# keyword 关键词, 不同的来源在使用中可以以此查出具体的任务,需要每个来源自己约束
# task_data 任务数据字典,字段可以自由设计
# sender 告警通道信息为字典可通过get_by_func字段指定从某个函数获取用于发送
# time_rule 告警的时间规则,包含 间隔时间(send_interval), (time-range)
# number_rule 告警的次数规则,包含 每日次数(day_num), 总次数(total), 通过函数判断(get_by_func)
# status 状态是否开启
# pre_hook, after_hook 前置处理和后置处理
# record_time, 告警记录存储时间, 默认为0 认为长时间储存
# last_check, 上次执行检查的时间
# last_send, 上次次发送时间
# number_data, 发送次数信息
create_task_sql = (
"CREATE TABLE IF NOT EXISTS 'task' ("
"'id' INTEGER PRIMARY KEY AUTOINCREMENT, "
"'template_id' INTEGER NOT NULL DEFAULT 0, "
"'source' TEXT NOT NULL DEFAULT '', "
"'keyword' TEXT NOT NULL DEFAULT '', "
"'title' TEXT NOT NULL DEFAULT '', "
"'task_data' TEXT NOT NULL DEFAULT '{}', "
"'sender' TEXT NOT NULL DEFAULT '[]', "
"'time_rule' TEXT NOT NULL DEFAULT '{}', "
"'number_rule' TEXT NOT NULL DEFAULT '{}', "
"'status' INTEGER NOT NULL DEFAULT 1, "
"'pre_hook' TEXT NOT NULL DEFAULT '{}', "
"'after_hook' TEXT NOT NULL DEFAULT '{}', "
"'last_check' INTEGER NOT NULL DEFAULT 0, "
"'last_send' INTEGER NOT NULL DEFAULT 0, "
"'number_data' TEXT NOT NULL DEFAULT '{}', "
"'create_time' INTEGER NOT NULL DEFAULT (strftime('%s')), "
"'record_time' INTEGER NOT NULL DEFAULT 0"
");"
)
create_task_record_sql = (
"CREATE TABLE IF NOT EXISTS 'task_record' ("
"'id' INTEGER PRIMARY KEY AUTOINCREMENT, "
"'template_id' INTEGER NOT NULL DEFAULT 0, "
"'task_id' INTEGER NOT NULL DEFAULT 0, "
"'do_send' TEXT NOT NULL DEFAULT '{}', "
"'send_data' TEXT NOT NULL DEFAULT '{}', "
"'result' TEXT NOT NULL DEFAULT '{}', "
"'create_time' INTEGER NOT NULL DEFAULT (strftime('%s'))"
");"
)
create_send_record_sql = (
"CREATE TABLE IF NOT EXISTS 'send_record' ("
"'id' INTEGER PRIMARY KEY AUTOINCREMENT, "
"'record_id' INTEGER NOT NULL DEFAULT 0, "
"'sender_name' TEXT NOT NULL DEFAULT '', "
"'sender_id' INTEGER NOT NULL DEFAULT 0, "
"'sender_type' TEXT NOT NULL DEFAULT '', "
"'send_data' TEXT NOT NULL DEFAULT '{}', "
"'result' TEXT NOT NULL DEFAULT '{}', "
"'create_time' INTEGER NOT NULL DEFAULT (strftime('%s'))"
");"
)
create_sender_sql = (
"CREATE TABLE IF NOT EXISTS 'sender' ("
"'id' INTEGER PRIMARY KEY AUTOINCREMENT, "
"'used' INTEGER NOT NULL DEFAULT 1, "
"'sender_type' TEXT NOT NULL DEFAULT '', "
"'name' TEXT NOT NULL DEFAULT '', "
"'data' TEXT NOT NULL DEFAULT '{}', "
"'create_time' INTEGER NOT NULL DEFAULT (strftime('%s'))"
");"
)
lock_push_db()
with get_push_db() as db:
db.execute("pragma journal_mode=wal")
res = db.execute(create_task_template_sql)
if isinstance(res, str) and res.startswith("error"):
write_log("warning system", "task_template Data table creation error:" + res)
DB_INIT_ERROR = True
return
res = db.execute(create_task_sql)
if isinstance(res, str) and res.startswith("error"):
write_log("warning system", "task Data table creation error:" + res)
DB_INIT_ERROR = True
return
res = db.execute(create_task_record_sql)
if isinstance(res, str) and res.startswith("error"):
write_log("warning system", "task_recorde Data table creation error:" + res)
DB_INIT_ERROR = True
return
res = db.execute(create_send_record_sql)
if isinstance(res, str) and res.startswith("error"):
write_log("warning system", "send_record Data table creation error:" + res)
DB_INIT_ERROR = True
return
res = db.execute(create_sender_sql)
if isinstance(res, str) and res.startswith("error"):
write_log("warning system", "sender Data table creation error:" + res)
DB_INIT_ERROR = True
return
db.execute(
"INSERT INTO 'sender' (id, sender_type, data) VALUES (?,?,?)",
(1, 'sms', json.dumps({"count": 0, "total": 0}))
) # 插入短信
unlock_push_db()
init_template_file = "/www/server/panel/config/mod_push_init.json"
err = load_task_template_by_file(init_template_file)
if err:
write_log("warning system", "task template data table initial data load failed:" + res)
def _check_fields(template: dict) -> bool:
if not isinstance(template, dict):
return False
fields = ("id", "ver", "used", "source", "title", "load_cls", "template", "default", "unique", "create_time")
for field in fields:
if field not in template:
return False
return True
def load_task_template_by_config(templates: List[Dict]) -> None:
"""
通过 传入的配置信息 执行一次模板更新操作
@param templates: 模板内容,为一个数据列表
@return: 报错信息如果返回None则表示执行成功
"""
task_template_config = TaskTemplateConfig()
add_list = []
for template in templates:
tmp = task_template_config.get_by_id(template['id'])
if tmp is not None:
tmp.update(template)
else:
add_list.append(template)
task_template_config.config.extend(add_list)
task_template_config.save_config()
# with get_table('task_template') as table:
# for template in templates:
# if not _check_fields(template):
# continue
# res = table.where("id = ?", (template['id'])).field('ver').select()
# if isinstance(res, str):
# return "数据库损坏:" + res
# if not res: # 没有就插入
# table.insert(template)
# else:
# # 版本不一致就更新版本
# if res['ver'] != template['ver']:
# template.pop("id")
# table.where("id = ?", (template['id'])).update(template)
#
def load_task_template_by_file(template_file: str) -> Optional[str]:
"""
执行一次模板更新操作
@param template_file: 模板文件路径
@return: 报错信息如果返回None则表示执行成功
"""
if not os.path.isfile(template_file):
return "The template file does not exist and the update fails"
if DB_INIT_ERROR:
return "An error is reported during database initialization and cannot be updated"
res = read_file(template_file)
if not isinstance(res, str):
return "Data read failed"
try:
templates = json.loads(res)
except (json.JSONDecoder, TypeError, ValueError):
return "Only JSON data is supported"
if not isinstance(templates, list):
return "The data is in the wrong format and should be a list"
return load_task_template_by_config(templates)

View File

@@ -0,0 +1,302 @@
import json
import os
import re
from datetime import datetime, timedelta
from typing import Tuple, Union, Optional, Iterator
from .send_tool import WxAccountMsg
from .base_task import BaseTask
from .mods import TaskTemplateConfig
from .util import read_file
def rsync_ver_is_38() -> Optional[bool]:
"""
检查rsync的版本是否为3.8。
该函数不接受任何参数。
返回值:
- None: 如果无法确定rsync的版本或文件不存在。
- bool: 如果版本确定为3.8则返回True否则返回False。
"""
push_file = "/www/server/panel/plugin/rsync/rsync_push.py"
if not os.path.exists(push_file):
return None
ver_info_file = "/www/server/panel/plugin/rsync/info.json"
if not os.path.exists(ver_info_file):
return None
try:
info = json.loads(read_file(ver_info_file))
except (json.JSONDecodeError, TypeError):
return None
ver = info["versions"]
ver_tuples = [int(i) for i in ver.split(".")]
if len(ver_tuples) < 3:
ver_tuples = ver_tuples.extend([0] * (3 - len(ver_tuples)))
if ver_tuples[0] < 3:
return None
if ver_tuples[1] <= 8 and ver_tuples[0] == 3:
return True
return False
class Rsync38Task(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "rsync_push"
self.template_name = "File synchronization alarm"
self.title = "File synchronization alarm"
def check_task_data(self, task_data: dict) -> Union[dict, str]:
if "interval" not in task_data or not isinstance(task_data["interval"], int):
task_data["interval"] = 600
return task_data
def get_keyword(self, task_data: dict) -> str:
return "rsync_push"
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
has_err = self._check(task_data.get("interval", 600))
if not has_err:
return None
return {
"msg_list": [
">Notification type: File synchronization alarm",
">Content of alarm: <font color=#ff0000>If an error occurs during file synchronization, please pay attention to the file synchronization situation and handle it in a timely manner.</font> ",
]
}
@staticmethod
def _check(interval: int) -> bool:
if not isinstance(interval, int):
return False
start_time = datetime.now() - timedelta(seconds=interval * 1.2)
log_file = "{}/plugin/rsync/lsyncd.log".format("/www/server/panel")
if not os.path.exists(log_file):
return False
return LogChecker(log_file=log_file, start_time=start_time)()
def check_time_rule(self, time_rule: dict) -> Union[dict, str]:
if "send_interval" not in time_rule or not isinstance(time_rule["interval"], int):
time_rule["send_interval"] = 3 * 60
if time_rule["send_interval"] < 60:
time_rule["send_interval"] = 60
return time_rule
def filter_template(self, template: dict) -> Optional[dict]:
res = rsync_ver_is_38()
if res is None:
return None
if res:
return template
else:
return None
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return '', {}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "File synchronization alarm"
msg.msg = "There was an error in the synchronization. Please keep an eye on the synchronization"
return msg
class Rsync39Task(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "rsync_push"
self.template_name = "File synchronization alarm"
self.title = "File synchronization alarm"
def check_task_data(self, task_data: dict) -> Union[dict, str]:
if "interval" not in task_data or not isinstance(task_data["interval"], int):
task_data["interval"] = 600
return task_data
def get_keyword(self, task_data: dict) -> str:
return "rsync_push"
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
"""
不返回数据,以实时触发为主
"""
return None
def check_time_rule(self, time_rule: dict) -> Union[dict, str]:
if "send_interval" not in time_rule or not isinstance(time_rule["send_interval"], int):
time_rule["send_interval"] = 3 * 60
if time_rule["send_interval"] < 60:
time_rule["send_interval"] = 60
return time_rule
def filter_template(self, template: dict) -> Optional[dict]:
res = rsync_ver_is_38()
if res is None:
return None
if res is False:
return template
else:
return None
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return '', {}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
task_name = push_data.get("task_name", None)
msg = WxAccountMsg.new_msg()
msg.thing_type = "File synchronization alarm"
if task_name:
msg.msg = "An error occurred on file synchronization task {}".format(task_name)
else:
msg.msg = "There was an error in the synchronization. Please keep an eye on the synchronization"
return msg
class LogChecker:
"""
排序查询并获取日志内容
"""
rep_time = re.compile(r'(?P<target>(\w{3}\s+){2}(\d{1,2})\s+(\d{2}:?){3}\s+\d{4})')
format_str = '%a %b %d %H:%M:%S %Y'
err_datetime = datetime.fromtimestamp(0)
err_list = ("error", "Error", "ERROR", "exitcode = 10", "failed")
def __init__(self, log_file: str, start_time: datetime):
self.log_file = log_file
self.start_time = start_time
self.is_over_time = None # None:还没查到时间,未知, False: 可以继续网上查询, True:比较早的数据了,不再向上查询
self.has_err = False # 目前已查询的内容中是否有报错信息
def _format_time(self, log_line) -> Optional[datetime]:
try:
date_str_res = self.rep_time.search(log_line)
if date_str_res:
time_str = date_str_res.group("target")
return datetime.strptime(time_str, self.format_str)
except Exception:
return self.err_datetime
return None
# 返回日志内容
def __call__(self):
_buf = b""
file_size, fp = os.stat(self.log_file).st_size - 1, open(self.log_file, mode="rb")
fp.seek(-1, 2)
while file_size:
read_size = min(1024, file_size)
fp.seek(-read_size, 1)
buf: bytes = fp.read(read_size) + _buf
fp.seek(-read_size, 1)
if file_size > 1024:
idx = buf.find(ord("\n"))
_buf, buf = buf[:idx], buf[idx + 1:]
for i in self._get_log_line_from_buf(buf):
self._check(i)
if self.is_over_time:
return self.has_err
file_size -= read_size
return False
# 从缓冲中读取日志
@staticmethod
def _get_log_line_from_buf(buf: bytes) -> Iterator[str]:
n, m = 0, 0
buf_len = len(buf) - 1
for i in range(buf_len, -1, -1):
if buf[i] == ord("\n"):
log_line = buf[buf_len + 1 - m: buf_len - n + 1].decode("utf-8")
yield log_line
n = m = m + 1
else:
m += 1
yield buf[0: buf_len - n + 1].decode("utf-8")
# 格式化并筛选查询条件
def _check(self, log_line: str) -> None:
# 筛选日期
for err in self.err_list:
if err in log_line:
self.has_err = True
ck_time = self._format_time(log_line)
if ck_time:
self.is_over_time = self.start_time > ck_time
def load_rsync_template():
"""
加载rsync模板
"""
if TaskTemplateConfig().get_by_id("40"):
return None
from .mods import load_task_template_by_config
load_task_template_by_config(
[{
"id": "40",
"ver": "1",
"used": True,
"source": "rsync_push",
"title": "File synchronization alarm",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.rsync_push",
"name": "RsyncTask"
},
"template": {
"field": [
],
"sorted": [
]
},
"default": {
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"wx_account",
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg",
],
"unique": True
}]
)
RsyncTask = Rsync39Task
if rsync_ver_is_38() is True:
RsyncTask = Rsync38Task
def push_rsync_by_task_name(task_name: str):
from .system import push_by_task_keyword
push_data = {
"task_name": task_name,
"msg_list": [
">Notification type: File synchronization alarm",
">Content of alarm: <font color=#ff0000>File synchronization task {} has failed during the execution, please pay attention to the file synchronization situation and deal with it.</font> ".format(
task_name),
]
}
push_by_task_keyword("rsync_push", "rsync_push", push_data=push_data)
class ViewMsgFormat(object):
@staticmethod
def get_msg(task: dict) -> Optional[str]:
if task["template_id"] == "40":
return "<span>Push alarm information when there is an exception in file synchronization (push {} times per day and then not push)<span>".format(
task.get("number_rule", {}).get("day_num"))
return None

View File

@@ -0,0 +1,615 @@
import json
from typing import Tuple, Union, Optional, Dict, List
from . import WxAccountMsg
from .mods import TaskConfig
from .base_task import BaseTask, BaseTaskViewMsg
from .site_push import web_info
from .util import get_db_by_file, DB, GET_CLASS, read_file, write_file
from .manager import PushManager
from .system import push_by_task_keyword
class SiteMonitorViolationWordTask(BaseTask): # 网站违规词告警检查
DB_FILE = "/www/server/panel/class/projectModel/content/content.db"
def __init__(self):
super().__init__()
self.source_name: str = 'site_monitor_violation_word'
self.title: str = 'Website violation keyword alert'
self.template_name: str = 'Website violation keyword alert'
def check_task_data(self, task_data: dict) -> Union[dict, str]:
"""
检查设置的告警参数(是否合理)
@param task_data: 传入的告警参数,提前会经过默认值处理(即没有的字段添加默认值)
@return: 当检查无误时,返回一个 dict 当做后续的添加和修改的数据,
当检查有误时, 直接返回错误信息的字符串
"""
if "interval" in task_data:
task_data.pop("interval")
if "mvw_id" in task_data and not task_data["mvw_id"]:
task_data.pop("mvw_id")
if "site_name" in task_data and not task_data["site_name"]:
task_data.pop("site_name")
return task_data
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
return None
def get_title(self, task_data: dict) -> str:
return "Violation keyword check on website {}".format(task_data["site_name"])
def get_keyword(self, task_data: dict) -> str:
return "site_mvw_{}".format(task_data.get("mvw_id", 0))
def filter_template(self, template: dict) -> Optional[dict]:
"""
过滤 和 更改模板中的信息, 返回空表是当前无法设置该任务
@param template: 任务的模板信息
@return:
"""
_, items_type = web_info(project_types=("PHP",))
template["field"][0]["items"].extend(items_type["PHP"])
return template
def task_config_create_hook(self, task: dict) -> Optional[str]:
task_data = task["task_data"]
if "mvw_id" not in task_data or not task_data["mvw_id"]:
return "No corresponding website violation word scanning task, unable to add alert"
if "site_name" not in task_data or not task_data["site_name"]:
return "No corresponding website violation word scanning task, unable to add alert"
def task_config_update_hook(self, task: dict) -> Optional[str]:
task_data = task["task_data"]
if "mvw_id" not in task_data or not task_data["mvw_id"]:
return "No corresponding website violation word scanning task, unable to add alert"
if "site_name" not in task_data or not task_data["site_name"]:
return "No corresponding website violation word scanning task, unable to add alert"
db_obj = get_db_by_file(self.DB_FILE)
if not db_obj:
return "Website violation word scanning task database file does not exist"
pdata = {
"send_msg": int(task["status"]),
"send_type": ",".join(task["sender"])
}
try:
db_obj.table("monitor_site").where("id=?", task_data["mvw_id"]).update(pdata)
db_obj.close()
except:
return "Website violation word scanning task update failed"
# 保障keyword的id正确
task["keyword"] = self.get_keyword(task_data)
def task_config_remove_hook(self, task: dict) -> None:
task_data = task["task_data"]
if "mvw_id" not in task_data or not task_data["mvw_id"]:
return
db_obj = get_db_by_file(self.DB_FILE)
if not db_obj:
return
try:
db_obj.table("monitor_site").where("id=?", task_data["mvw_id"]).update({"send_msg": 0})
db_obj.close()
except:
return
@classmethod
def set_push_task(cls, mvw_id: int, site_name: str, status: bool, sender: list):
task_conf = TaskConfig()
old_task = task_conf.get_by_keyword("site_monitor_violation_word", "site_mvw_{}".format(mvw_id))
if not old_task:
push_data = {
"template_id": "121",
"task_data": {
"status": True,
"sender": sender,
"task_data": {
"site_name": site_name,
"mvw_id": mvw_id,
}
}
}
from .manager import PushManager
PushManager().set_task_conf_data(push_data)
else:
old_task["sender"] = sender
old_task["status"] = status
task_conf.save_config()
@classmethod
def remove_push_task(cls, mvw_id: int):
task_conf = TaskConfig()
old_task = task_conf.get_by_keyword("site_monitor_violation_word", "site_mvw_{}".format(mvw_id))
if old_task:
task_conf.config.remove(old_task)
task_conf.save_config()
class VulnerabilityScanningTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name: str = 'vulnerability_scanning'
self.title: str = 'Website vulnerability alert'
self.template_name: str = 'Website vulnerability alert'
def check_task_data(self, task_data: dict) -> Union[dict, str]:
"""
检查设置的告警参数(是否合理)
@param task_data: 传入的告警参数,提前会经过默认值处理(即没有的字段添加默认值)
@return: 当检查无误时,返回一个 dict 当做后续的添加和修改的数据,
当检查有误时, 直接返回错误信息的字符串
"""
if "cycle" not in task_data or not task_data["cycle"] or not isinstance(task_data["cycle"], int):
return "Cycle cannot be empty"
return {"interval": 60*60*24 * (task_data["cycle"] + 1), "cycle": task_data["cycle"]}
def get_keyword(self, task_data: dict) -> str:
"""
返回一个关键字,用于后续查询或执行任务时使用, 例如防篡改告警可以根据其规则id生成一个关键字
后续通过规则id和来源tamper 查询并使用
@param task_data: 通过check_args后生成的告警参数字典
@return: 返回一个关键词字符串
"""
return "vulnerability_scanning"
def get_title(self, task_data: dict) -> str:
"""
返回一个标题
@param task_data: 通过check_args后生成的告警参数字典
@return: 返回一个关键词字符串
"""
return 'Website vulnerability alert'
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
"""
判断这个任务是否需要返送
@param task_id: 任务id
@param task_data: 任务的告警参数
@return: 如果触发了告警返回一个dict的原文作为告警信息否则应当返回None表示未触发
返回之中应当包含一个 msg_list 的键值为List[str]类型),将主要的信息返回
用于以下信息的自动序列化包含[dingding, feishu, mail, weixin, web_hook]
短信和微信公众号由于长度问题,必须每个任务手动实现
"""
return None
def filter_template(self, template: dict) -> Optional[dict]:
"""
过滤 和 更改模板中的信息, 返回空表是当前无法设置该任务
@param template: 任务的模板信息
@return:
"""
return template
@classmethod
def set_push_task(cls, status: bool, day: int, sender: list):
push_data = {
"template_id": "122",
"task_data": {
"status": status,
"sender": sender,
"task_data": {
"cycle": day,
}
}
}
return PushManager().set_task_conf_data(push_data)
@staticmethod
def del_crontab():
"""
@name 删除项目定时清理任务
@auther hezhihong<2022-10-31>
@return
"""
cron_name = '[Do not delete] Vulnerability scanning scheduled task'
cron_list = DB('crontab').where("name=?", (cron_name,)).select()
if cron_list:
for i in cron_list:
if not i:
continue
args = {"id": i['id']}
import crontab
crontab.crontab().DelCrontab(args)
def add_crontab(self, day, channel):
"""
@name 构造计划任务
"""
cron_name = '[Do not delete] Vulnerability scanning scheduled task'
cron_list = DB('crontab').where("name=?", (cron_name,)).select()
if cron_list:
self.del_crontab()
if not DB('crontab').where('name=?',(cron_name,)).count():
args = {
"name": cron_name,
"type": 'day-n',
"where1": day,
"hour": '10',
"minute": '30',
"sName": "",
"sType": 'toShell',
"notice": '0',
"notice_channel": channel,
"save": '',
"save_local": '1',
"backupTo": '',
"sBody": 'btpython /www/server/panel/script/cron_scaning.py {}'.format(channel),
"urladdress": '',
"user": 'root'
}
import crontab
res = crontab.crontab().AddCrontab(args)
if res and "id" in res.keys():
return True
return False
return True
def task_config_create_hook(self, task: dict) -> Optional[str]:
return self.task_config_update_hook(task)
def task_config_update_hook(self, task: dict) -> Optional[str]:
if task["status"]:
day = task['task_data']['cycle']
channel = ",".join(task['sender'])
if self.add_crontab(day, channel):
return None
return "Failed to add scheduled task"
else:
self.del_crontab()
def task_config_remove_hook(self, task: dict) -> None:
self.del_crontab()
class FileDetectTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name: str = 'file_detect'
self.title: str = 'System file integrity reminder'
self.template_name: str = 'System file integrity reminder'
def check_task_data(self, task_data: dict) -> Union[dict, str]:
"""
检查设置的告警参数(是否合理)
@param task_data: 传入的告警参数,提前会经过默认值处理(即没有的字段添加默认值)
@return: 当检查无误时,返回一个 dict 当做后续的添加和修改的数据,
当检查有误时, 直接返回错误信息的字符串
"""
if not isinstance(task_data["hour"], int) or not isinstance(task_data["minute"], int):
return "Hours and minutes must be integers"
if task_data["hour"] < 0 or task_data["hour"] > 23:
return "The hour must be an integer between 0 and 23"
if task_data["minute"] < 0 or task_data["minute"] > 59:
return "Minutes must be an integer between 0 and 59"
return {
"interval": 60 * 60 * 24,
"hour": task_data["hour"],
"minute": task_data["minute"],
}
def get_keyword(self, task_data: dict) -> str:
"""
返回一个关键字,用于后续查询或执行任务时使用, 例如防篡改告警可以根据其规则id生成一个关键字
后续通过规则id和来源tamper 查询并使用
@param task_data: 通过check_args后生成的告警参数字典
@return: 返回一个关键词字符串
"""
return "file_detect"
def get_title(self, task_data: dict) -> str:
"""
返回一个标题
@param task_data: 通过check_args后生成的告警参数字典
@return: 返回一个关键词字符串
"""
return 'System file integrity reminder'
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
return None
def filter_template(self, template: dict) -> Optional[dict]:
"""
过滤 和 更改模板中的信息, 返回空表是当前无法设置该任务
@param template: 任务的模板信息
@return:
"""
return template
def add_crontab(self, hour, minute, channel):
"""
@name 构造计划任务
"""
cron_name = '[Do not delete] File integrity monitoring scheduled task'
cron_list = DB('crontab').where("name=?", (cron_name,)).select()
if cron_list:
self.del_crontab()
if not DB('crontab').where('name=?', (cron_name,)).count():
args = {
"name": cron_name,
"type": 'day',
"where1": '',
"hour": hour,
"minute": minute,
"sName": "",
"sType": 'toShell',
"notice": '0',
"notice_channel": channel,
"save": '',
"save_local": '1',
"backupTo": '',
"sBody": 'btpython /www/server/panel/script/cron_file.py {}'.format(channel),
"urladdress": ''
}
import crontab
res = crontab.crontab().AddCrontab(args)
if res and "id" in res.keys():
return True
return False
return True
# 删除项目定时清理任务
@staticmethod
def del_crontab():
cron_name = '[Do not delete] File integrity monitoring scheduled task'
cron_list = DB('crontab').where("name=?", (cron_name,)).select()
if cron_list:
for i in cron_list:
if not i: continue
args = {"id": i['id']}
import crontab
crontab.crontab().DelCrontab(args)
def task_config_create_hook(self, task: dict) -> Optional[str]:
return self.task_config_update_hook(task)
def task_config_update_hook(self, task: dict) -> Optional[str]:
if task["status"]:
hour = task['task_data']['hour']
minute = task['task_data']['minute']
channel = ",".join(task['sender'])
if self.add_crontab(hour, minute, channel):
return None
return "Failed to add scheduled task"
else:
self.del_crontab()
def task_config_remove_hook(self, task: dict) -> None:
self.del_crontab()
@classmethod
def set_push_task(cls, status: bool, hour: int, minute: int, sender: list):
push_data = {
"template_id": "123",
"task_data": {
"status": status,
"sender": sender,
"task_data": {
"hour": hour,
"minute": minute,
}
}
}
from .manager import PushManager
return PushManager().set_task_conf_data(push_data)
class SafeCloudTask(BaseTask):
_config_file = "/www/server/panel/data/safeCloud/config.json"
_all_safe_type = ("webshell", )
def __init__(self):
super().__init__()
self.source_name = "safe_cloud_hinge"
self.title = "Yakpanel Cloud Security Center Alarm"
self.template_name = "Yakpanel Cloud Security Center Alarm"
self._safe_cloud_conf: Optional[dict] = None
@property
def safe_cloud_conf(self) -> Optional[dict]:
"""
获取云安全配置
:return: 云安全配置
"""
if self._safe_cloud_conf and isinstance(self._safe_cloud_conf, dict):
return self._safe_cloud_conf
try:
self._safe_cloud_conf = json.loads(read_file(self._config_file))
return self._safe_cloud_conf
except:
self._init_config()
try:
self._safe_cloud_conf = json.loads(read_file(self._config_file))
return self._safe_cloud_conf
except:
pass
return None
def filter_template(self, template: dict) -> Optional[dict]:
"""
过滤模板
:param template: 模板
:return: 过滤后的模板
"""
return template
def save_safe_cloud_conf(self):
"""
保存云安全配置
"""
write_file(self._config_file, json.dumps(self._safe_cloud_conf))
def check_task_data(self, task_data: dict) -> Union[dict, str]:
"""
检查任务数据
:param task_data: 任务数据
:return: 检查后的任务数据
"""
if "safe_type" in task_data:
for i in task_data["safe_type"]:
if i not in self._all_safe_type:
return "Security type error"
else:
task_data["safe_type"] = ["webshell"]
task_data["interval"] = 60 * 60 * 3
return task_data
def check_num_rule(self, num_rule: dict) -> Union[dict, str]:
"""
检查告警数量规则一天只能告警20次
:param num_rule: 告警数量规则
:return: 检查后的告警数量规则
"""
num_rule["day_num"] = 20
return num_rule
def check_time_rule(self, time_rule: dict) -> Union[dict, str]:
"""
检查告警时间规则[写死]
:param time_rule: 告警时间规则
:return: 检查后的告警时间规则
"""
# 测试数据为1秒 ,正常数据为 1200 20*60 20分钟告警一次
time_rule["send_interval"] = 1200
return time_rule
def get_keyword(self, task_data: dict) -> str:
"""
返回一个关键字,用于后续查询或执行任务时使用, 例如防篡改告警可以根据其规则id生成一个关键字
后续通过规则id和来源tamper 查询并使用
@param task_data: 通过check_args后生成的告警参数字典
@return: 返回一个关键词字符串
"""
return "safe_cloud_hinge"
def get_title(self, task_data: dict) -> str:
"""
返回一个标题
@param task_data: 通过check_args后生成的告警参数字典
@return: 返回一个关键词字符串
"""
return 'Yakpanel Cloud Security Center Alarm'
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
"""
判断这个任务是否需要返送
@param task_id: 任务id
@param task_data: 任务的告警参数
@return: 如果触发了告警返回一个dict的原文作为告警信息否则应当返回None表示未触发
返回之中应当包含一个 msg_list 的键值为List[str]类型),将主要的信息返回
用于以下信息的自动序列化包含[dingding, feishu, mail, weixin, web_hook]
短信和微信公众号由于长度问题,必须每个任务手动实现
"""
return None
def task_config_create_hook(self, task: dict) -> Optional[str]:
return self.task_config_update_hook(task)
def task_config_update_hook(self, task: dict) -> Optional[str]:
"""
更新告警配置
:param task: 任务
:return: 更新后的任务
"""
if not self.safe_cloud_conf:
return "Failed to initialize configuration file, unable to add"
alert_data = self.safe_cloud_conf["alertable"]
alert_data["safe_type"] = task["task_data"].get("safe_type", ["webshell"])
alert_data["interval"] = task["task_data"].get("interval", 60*60*3)
alert_data["status"] = task["status"]
alert_data["sender"] = task["sender"]
alert_data["time_rule"] = task["time_rule"]
alert_data["number_rule"] = task["number_rule"]
self.save_safe_cloud_conf()
def task_config_remove_hook(self, task: dict) -> Optional[str]:
"""
删除告警配置
:param task: 任务
:return: 删除后的任务
"""
if not self.safe_cloud_conf:
return None
alert_data = self.safe_cloud_conf["alertable"]
alert_data["safe_type"] = task["task_data"].get("safe_type", ["webshell"])
alert_data["interval"] = task["task_data"].get("interval", 60*60*3)
alert_data["status"] = False
alert_data["sender"] = []
alert_data["time_rule"] = task["time_rule"]
alert_data["number_rule"] = task["number_rule"]
self.save_safe_cloud_conf()
# 更新告警配置
@staticmethod
def set_push_conf(alert_data: dict) -> Optional[str]:
"""
将告警信息设置到告警任务中去
:param alert_data:
:return:
"""
pm = PushManager()
p_data = {
"template_id": "124",
"task_data": {
"status": alert_data.get("status", True),
"sender": alert_data.get("sender", []),
"task_data": {
"safe_type": alert_data.get("safe_type", ["webshell"]),
"interval": alert_data.get("interval", 60*60*3),
},
"time_rule": alert_data.get("time_rule", {}),
"number_rule": alert_data.get("number_rule", {}),
}
}
return pm.set_task_conf_data(p_data)
@staticmethod
def _init_config():
"""
初始化配置
"""
try:
import PluginLoader
args = GET_CLASS()
args.model_index = 'project'
PluginLoader.module_run("safecloud", "init_config", args)
except:
pass
class ViewMsgFormat(BaseTaskViewMsg):
def get_msg(self, task: dict) -> Optional[str]:
if task["template_id"] == "122":
return "Every {} days, identify and scan for vulnerabilities in common open-source CMS programs across all websites and send alerts".format(task["task_data"]["cycle"])
if task["template_id"] == "123":
return "Scan the critical executable files in the system daily at 【{}:{}】, and send alerts when changes are detected".format(
task["task_data"]["hour"], task["task_data"]["minute"]
)
if task["template_id"] == "124":
return "Every {} hours, scan the server files to identify abnormal situations such as high resource consumption and malicious control of the server, and send alerts".format(
int(task["task_data"]["interval"] / 3600)
)
return None
SiteMonitorViolationWordTask.VIEW_MSG = VulnerabilityScanningTask.VIEW_MSG = FileDetectTask.VIEW_MSG = SafeCloudTask.VIEW_MSG = ViewMsgFormat

View File

@@ -0,0 +1,196 @@
[
{
"id": "122",
"ver": "1",
"used": true,
"source": "vulnerability_scanning",
"title": "Website vulnerability alert",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.safe_mod_push",
"name": "VulnerabilityScanningTask"
},
"template": {
"field": [
{
"attr": "cycle",
"name": "Interval period",
"type": "number",
"suffix": "",
"unit": "day",
"default": 1
},
{
"attr": "help",
"name": "source",
"type": "help",
"unit": "",
"style": {
"margin-top": "6px"
},
"list": [
"【Website】-> 【Vulnerability Scanning】-> 【Automatic Scanning】"
],
"suffix": "",
"default": 600
}
],
"sorted": [
[
"cycle"
],
[
"help"
]
]
},
"default": {
},
"advanced_default": {
"number_rule": {
"day_num": 1
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": true,
"tags": [
"safe",
"site"
],
"description": "Regularly scan all websites on the server and identify various popular open-source CMS programs to help users quickly discover potential security vulnerabilities on the website and send alert notifications",
"is_pro": true
},
{
"id": "123",
"ver": "1",
"used": true,
"source": "file_detect",
"title": "System file integrity reminder",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.safe_mod_push",
"name": "FileDetectTask"
},
"template": {
"field": [
{
"attr": "hour",
"name": "daily",
"type": "number",
"suffix": "",
"unit": "hour",
"default": 4
},
{
"attr": "minute",
"name": " ",
"type": "number",
"suffix": "Execute detection tasks",
"unit": "minute",
"default": 10
},
{
"attr": "help",
"name": "source",
"type": "help",
"unit": "",
"style": {
"margin-top": "6px"
},
"list": [
"【Security】-> 【Security testing】-> 【File integrity check】-> 【Regular scanning】"
],
"suffix": "",
"default": 600
}
],
"sorted": [
[
"hour",
"minute"
],
[
"help"
]
]
},
"default": {
},
"advanced_default": {
"number_rule": {
"day_num": 1
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": true,
"tags": [
"safe"
],
"is_pro": true,
"description": "Regularly scan the system's critical file directory (such as /var/bin) according to the rules, identify abnormal situations such as file tampering, deletion, or movement, and send alert notifications to the administrator"
},
{
"id": "124",
"ver": "1",
"used": true,
"source": "safe_cloud_hinge",
"title": "Cloud Security Center Alert",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.safe_mod_push",
"name": "SafeCloudTask"
},
"template": {
"field": [
{
"attr": "help",
"name": "help",
"type": "link",
"unit": "",
"style": {
"margin-top": "6px"
},
"list": [
"<a href=\"/\">Go to 【 Home>Baota Cloud Security Center 】 to view details</a>"
]
}
],
"sorted": [
[
"help"
]
]
},
"default": {},
"advanced_default": {},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"wx_account",
"tg"
],
"unique": true,
"tags": [
"safe"
],
"is_pro": true,
"description": "The Baota Cloud Security Center will scan server files, identify abnormal situations such as occupying a large amount of resources or maliciously controlling servers, and send alert notifications to administrators."
}
]

View File

@@ -0,0 +1,133 @@
import ipaddress
import re
from .util import get_config_value
class WxAccountMsgBase:
@classmethod
def new_msg(cls):
return cls()
def set_ip_address(self, server_ip, local_ip):
pass
def to_send_data(self):
return "", {}
class WxAccountMsg(WxAccountMsgBase):
def __init__(self):
self.ip_address: str = ""
self.thing_type: str = ""
self.msg: str = ""
self.next_msg: str = ""
def set_ip_address(self, server_ip, local_ip):
self.ip_address = "{}({})".format(server_ip, local_ip)
if len(self.ip_address) > 32:
self.ip_address = self.ip_address[:29] + "..."
def to_send_data(self):
res = {
"first": {},
"keyword1": {
"value": self.ip_address,
},
"keyword2": {
"value": self.thing_type,
},
"keyword3": {
"value": self.msg,
}
}
if self.next_msg != "":
res["keyword4"] = {"value": self.next_msg}
return "", res
class WxAccountLoginMsg(WxAccountMsgBase):
tid = "RJNG8dBZ5Tb9EK6j6gOlcAgGs2Fjn5Fb07vZIsYg1P4"
def __init__(self):
self.login_name: str = ""
self.login_ip: str = ""
self.thing_type: str = ""
self.login_type: str = ""
self.address: str = ""
self._server_name: str = ""
def set_ip_address(self, server_ip, local_ip):
if self._server_name == "":
self._server_name = "服务器IP{}".format(server_ip)
def _get_server_name(self):
data = get_config_value("title") # 若获得别名,则使用别名
if data != "":
self._server_name = data
def to_send_data(self):
self._get_server_name()
if self.address.startswith(">Place of Ownership:"):
self.address = self.address[5:]
if self.address == "":
self.address = "Unknown place of ownership"
if not _is_ipv4(self.login_ip):
self.login_ip = "ipv6-can not show"
res = {
"thing10": {
"value": self._server_name,
},
"character_string9": {
"value": self.login_ip,
},
"thing7": {
"value": self.login_type,
},
"thing11": {
"value": self.address,
},
"thing2": {
"value": self.login_name,
}
}
return self.tid, res
# 处理短信告警信息的不规范问题
def sms_msg_normalize(sm_args: dict) -> dict:
for key, val in sm_args.items():
sm_args[key] = _norm_sms_push_argv(str(val))
return sm_args
def _norm_sms_push_argv(data):
"""
@处理短信参数,否则会被拦截
"""
if _is_ipv4(data):
tmp1 = data.split('.')
return '{}_***_***_{}'.format(tmp1[0], tmp1[3])
data = data.replace(".", "_").replace("+", "")
return data
def _is_ipv4(data: str) -> bool:
try:
ipaddress.IPv4Address(data)
except:
return False
return True
def _is_domain(domain):
rep_domain = re.compile(r"^([\w\-*]{1,100}\.){1,10}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$")
if rep_domain.match(domain):
return True
return False

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,538 @@
[
{
"id": "1",
"ver": "1",
"used": true,
"source": "ssl",
"title": "Certificate (SSL) expiration",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.site_push",
"name": "SSLCertificateTask"
},
"template": {
"field": [
{
"attr": "project",
"name": "Model",
"type": "select",
"default": "all",
"items": [
{
"title": "ALL SSL",
"value": "all"
}
]
},
{
"attr": "cycle",
"name": "Remaining days",
"type": "number",
"suffix": "",
"unit": "day(s)",
"default": 15
}
],
"sorted": [
[
"project"
],
[
"cycle"
]
]
},
"default": {
"project": "all",
"cycle": 15
},
"advanced_default": {
"number_rule": {
"total": 2
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"sms",
"tg"
],
"unique": false
},
{
"id": "2",
"ver": "1",
"used": true,
"source": "site_end_time",
"title": "Website expiration",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.site_push",
"name": "SiteEndTimeTask"
},
"template": {
"field": [
{
"attr": "cycle",
"name": "Remaining days",
"type": "number",
"unit": "day(s)",
"suffix": "",
"default": 7
}
],
"sorted": [
[
"cycle"
]
]
},
"default": {
"cycle": 7
},
"advanced_default": {
"number_rule": {
"total": 2
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": true
},
{
"id": "3",
"ver": "1",
"used": true,
"source": "panel_pwd_end_time",
"title": "YakPanel password expiration date",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.site_push",
"name": "PanelPwdEndTimeTask"
},
"template": {
"field": [
{
"attr": "cycle",
"name": "Remaining days",
"type": "number",
"unit": "day(s)",
"suffix": "",
"default": 15
}
],
"sorted": [
[
"cycle"
]
]
},
"default": {
"cycle": 15
},
"advanced_default": {
"number_rule": {
"total": 2
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": true
},
{
"id": "4",
"ver": "1",
"used": true,
"source": "ssh_login_error",
"title": "SSH login failure alarm",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.site_push",
"name": "SSHLoginErrorTask"
},
"template": {
"field": [
{
"attr": "cycle",
"name": "Trigger conditions",
"type": "number",
"unit": "minute(s)",
"suffix": "less than ",
"default": 30
},
{
"attr": "count",
"name": "Login failed",
"type": "number",
"unit": "time(s)",
"suffix": "",
"default": 3
},
{
"attr": "interval",
"name": "Interval",
"type": "number",
"unit": "second(s)",
"suffix": "more than ",
"default": 600
}
],
"sorted": [
[
"cycle",
"count"
],
[
"interval"
]
]
},
"default": {
"cycle": 30,
"count": 3,
"interval": 600
},
"advanced_default": {
"number_rule": {
"day_num": 3
},
"time_rule": {
"send_interval": 600
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": true
},
{
"id": "5",
"ver": "1",
"used": true,
"source": "services",
"title": "Service Stop Alert",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.site_push",
"name": "ServicesTask"
},
"template": {
"field": [
{
"attr": "project",
"name": "Notification type",
"type": "select",
"default": null,
"items": [
]
},
{
"attr": "count",
"name": "Auto-restart",
"type": "radio",
"suffix": "",
"default": 1,
"items": [
{
"title": "Automatically attempt to restart the project",
"value": 1
},
{
"title": "Do not attempt to restart",
"value": 2
}
]
},
{
"attr": "interval",
"name": "Interval",
"type": "number",
"unit": "second(s)",
"suffix": "more than ",
"default": 600
}
],
"sorted": [
[
"project"
],
[
"count"
],
[
"interval"
]
]
},
"default": {
"project": "",
"count": 2,
"interval": 600
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": false
},
{
"id": "6",
"ver": "1",
"used": true,
"source": "panel_safe_push",
"title": "YakPanel security alarms",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.site_push",
"name": "PanelSafePushTask"
},
"template": {
"field": [
{
"attr": "help",
"name": "Alarm content",
"type": "help",
"unit": "",
"style": {
"margin-top": "6px"
},
"list": [
"Panel user changes, panel logs are deleted, panels are opened for developers"
],
"suffix": "",
"default": 600
}
],
"sorted": [
[
"help"
]
]
},
"default": {
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": true
},
{
"id": "7",
"ver": "1",
"used": true,
"source": "ssh_login",
"title": "SSH login alert",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.site_push",
"name": "SSHLoginTask"
},
"template": {
"field": [
],
"sorted": [
]
},
"default": {
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": true
},
{
"id": "8",
"ver": "1",
"used": true,
"source": "panel_login",
"title": "YakPanel login alarm",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.site_push",
"name": "PanelLoginTask"
},
"template": {
"field": [
],
"sorted": [
]
},
"default": {
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"sms",
"tg"
],
"unique": true
},
{
"id": "9",
"ver": "1",
"used": true,
"source": "project_status",
"title": "Project Stop Alert",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.site_push",
"name": "ProjectStatusTask"
},
"template": {
"field": [
{
"attr": "cycle",
"name": "project type",
"type": "select",
"default": 1,
"items": [
{
"title": "Node Project",
"value": 2
},
{
"title": "Go Project",
"value": 3
},
{
"title": "Python Project",
"value": 4
},
{
"title": "Other Project",
"value": 5
}
]
},
{
"attr": "project",
"name": "Project name",
"type": "select",
"default": null,
"all_items": null,
"items": [
]
},
{
"attr": "interval",
"name": "interval",
"type": "number",
"unit": "second(s)",
"suffix": "more than ",
"default": 600
},
{
"attr": "count",
"name": "Auto-restart",
"type": "radio",
"suffix": "",
"default": 1,
"items": [
{
"title": "Automatically attempt to restart the project",
"value": 1
},
{
"title": "Do not attempt to restart",
"value": 2
}
]
}
],
"sorted": [
[
"cycle"
],
[
"project"
],
[
"interval"
],
[
"count"
]
]
},
"default": {
"cycle": 1,
"project": "",
"interval": 600,
"count": 2
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"sms",
"tg"
],
"unique": false,
"tags": ["site", "common"]
}
]

View File

@@ -0,0 +1,348 @@
import json
import time
from datetime import datetime
from typing import Tuple, Union, Optional
from .send_tool import WxAccountMsg
from .base_task import BaseTask
from .mods import PUSH_DATA_PATH, TaskConfig, PANEL_PATH
from .util import read_file, DB, write_file
from mod.base.web_conf import RealSSLManger
import public
class DomainEndTimeTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "domain_endtime"
self.template_name = "Domain expiration"
# self.title = "Domain expiration"
self._tip_file = "{}/domain_endtime.tip".format(PUSH_DATA_PATH)
self._tip_data: Optional[dict] = None
self._task_config = TaskConfig()
# 每次任务使用
self.domain_list = []
self.push_keys = []
self.task_id = None
@property
def tips(self) -> dict:
if self._tip_data is not None:
return self._tip_data
try:
self._tip_data = json.loads(read_file(self._tip_file))
except:
self._tip_data = {}
return self._tip_data
def save_tip(self):
write_file(self._tip_file, json.dumps(self.tips))
def get_keyword(self, task_data: dict) -> str:
return task_data["project"]
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
self.title = self.get_title(task_data)
# 过滤单独设置提醒的域名
not_push_web = [i["task_data"]["project"] for i in self._task_config.config if i["source"] == self.source_name]
sql = DB("ssl_domains")
total = self._task_config.get_by_id(task_id).get("number_rule", {}).get("total", 1)
if "all" in not_push_web:
not_push_web.remove("all")
# need_check_list = []
if task_data["project"] == "all":
# 所有域名
domain_list = sql.select()
for domain in domain_list:
if domain['domain'] in not_push_web:
continue
if self.tips.get(task_id, {}).get(domain['domain'], 0) > total:
continue
end_time = datetime.strptime(domain['endtime'], '%Y-%m-%d')
if int((end_time.timestamp() - time.time()) / 86400) <= task_data['cycle']:
self.push_keys.append(domain['domain'])
self.domain_list.append(domain)
# need_check_list.append(domain['domain'])
else:
find = sql.where('domain=?', (task_data['project'],)).find()
if not find:
return None
end_time = datetime.strptime(find['endtime'], '%Y-%m-%d')
if int((end_time.timestamp() - time.time()) / 86400) <= task_data['cycle']:
self.push_keys.append(find['domain'])
self.domain_list.append(find)
# need_check_list.append((find['domain']))
# for name, project_type in need_check_list:
# info = self._check_end_time(name, task_data['cycle'], project_type)
# if isinstance(info, dict): # 返回的是详情,说明需要推送了
# info['site_name'] = name
# self.push_keys.append(name)
# self.domain_list.append(info)
if len(self.domain_list) == 0:
return None
s_list = ['>About to expire: <font color=#ff0000>{} </font>'.format(len(self.domain_list))]
for x in self.domain_list:
s_list.append(">Domain: {} Expiration:{}".format(x['domain'], x['endtime']))
self.task_id = task_id
return {"msg_list": s_list}
@staticmethod
def _check_end_time(site_name, limit, prefix) -> Optional[dict]:
info = RealSSLManger(conf_prefix=prefix).get_site_ssl_info(site_name)
if info is not None:
end_time = datetime.strptime(info['notAfter'], '%Y-%m-%d')
if int((end_time.timestamp() - time.time()) / 86400) <= limit:
return info
return None
def get_title(self, task_data: dict) -> str:
if task_data["project"] == "all":
return "Domain expiration -- All"
return "Domain expiration -- [{}]".format(task_data["project"])
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return 'domain_end|Domain expiration reminders', {
"name": push_public_data["ip"],
"domain": self.domain_list[0]['domain'],
'time': self.domain_list[0]["endtime"],
'total': len(self.domain_list)
}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "Domain expiration reminders"
msg.msg = "There are {} domain names that will expire and will affect access".format(len(self.domain_list))
return msg
def check_task_data(self, task_data: dict) -> Union[dict, str]:
task_data["interval"] = 60 * 60 * 24 # 默认检测间隔时间 1 天
if not (isinstance(task_data['cycle'], int) and task_data['cycle'] > 1):
return "The remaining time parameter is incorrect, at least 1 day"
return task_data
def filter_template(self, template) -> dict:
domain_list = DB("ssl_domains").select()
items = [{"title": i["domain"], "value": i["domain"]}for i in domain_list]
template["field"][0]["items"].extend(items)
return template
def check_num_rule(self, num_rule: dict) -> Union[dict, str]:
num_rule["get_by_func"] = "can_send_by_num_rule"
return num_rule
# 实际的次数检查已在 get_push_data 其他位置完成
def can_send_by_num_rule(self, task_id: str, task_data: dict, number_rule: dict, push_data: dict) -> Optional[str]:
return None
def task_run_end_hook(self, res) -> None:
if not res["do_send"]:
return
if self.task_id:
if self.task_id not in self.tips:
self.tips[self.task_id] = {}
for w in self.push_keys:
if w in self.tips[self.task_id]:
self.tips[self.task_id][w] += 1
else:
self.tips[self.task_id][w] = 1
self.save_tip()
def task_config_update_hook(self, task: dict) -> None:
if task["id"] in self.tips:
self.tips.pop(task["id"])
self.save_tip()
def task_config_remove_hook(self, task: dict) -> None:
if task["id"] in self.tips:
self.tips.pop(task["id"])
self.save_tip()
class CertEndTimeTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "cert_endtime"
self.template_name = "Certificate expiration"
# self.title = "Certificate expiration"
self._tip_file = "{}/cert_endtime.tip".format(PUSH_DATA_PATH)
self._tip_data: Optional[dict] = None
self._task_config = TaskConfig()
# 每次任务使用
self.cert_list = []
self.push_keys = []
self.task_id = None
@property
def tips(self) -> dict:
if self._tip_data is not None:
return self._tip_data
try:
self._tip_data = json.loads(read_file(self._tip_file))
except:
self._tip_data = {}
return self._tip_data
def save_tip(self):
write_file(self._tip_file, json.dumps(self.tips))
def get_keyword(self, task_data: dict) -> str:
return task_data["project"]
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
from .util import get_cert_list, to_dict_obj
exclude_ids = [i["task_data"]["project"] for i in self._task_config.config if i["source"] == self.source_name]
total = self._task_config.get_by_id(task_id).get("number_rule", {}).get("total", 1)
if "all" in exclude_ids:
exclude_ids.remove("all")
data = get_cert_list(to_dict_obj({"status_id": 1}))['data']
if task_data["project"] == "all":
for cert in data:
if cert["ssl_id"] in exclude_ids:
continue
if self.tips.get(task_id, {}).get(cert['ssl_id'], 0) > total:
continue
if not cert.get("endDay") and cert.get("endDay") != 0:
continue
if cert["endDay"] <= task_data["cycle"]:
self.cert_list.append(cert)
else:
for cert in data:
if cert["ssl_id"] != task_data["project"]:
continue
if not cert.get("endDay") and cert.get("endDay") != 0:
continue
if cert["endDay"] <= task_data["cycle"]:
self.cert_list.append(cert)
self.title = self.get_title(task_data)
if len(self.cert_list) == 0:
return None
s_list = ['>About to expire: <font color=#ff0000>{} </font>'.format(len(self.cert_list))]
for x in self.cert_list:
s_list.append(
">Certificates: {} [{}]expire in days Websites that may be affected:{}".format("{} | {}".format(x["title"],",".join(x.get("domainName", []) or "None")), x['endDay'], ','.join(x.get('use_site', [])) or "None")
)
self.task_id = task_id
return {"msg_list": s_list}
@staticmethod
def _check_end_time(site_name, limit, prefix) -> Optional[dict]:
info = RealSSLManger(conf_prefix=prefix).get_site_ssl_info(site_name)
if info is not None:
end_time = datetime.strptime(info['notAfter'], '%Y-%m-%d')
if int((end_time.timestamp() - time.time()) / 86400) <= limit:
return info
return None
def get_title(self, task_data: dict) -> str:
from .util import get_cert_list, to_dict_obj
if task_data["project"] == "all":
return "Certificate expiration -- All"
data = get_cert_list(to_dict_obj({}))['data']
for cert in data:
if cert["ssl_id"] == task_data["project"]:
return "Certificate expiration -- [{} | {}]".format(cert["title"],",".join(cert.get("domainName", []) or "None"))
return "Domain [{}] expiration reminder".format(task_data["project"])
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return 'cert_end|Certificate expiration reminders', {
"name": push_public_data["ip"],
"cert": self.cert_list[0]['domain'],
'time': self.cert_list[0]["endtime"],
'total': len(self.cert_list)
}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "Certificate expiration reminders"
msg.msg = "There are {} certificates that will expire and will affect access".format(len(self.cert_list))
return msg
def check_task_data(self, task_data: dict) -> Union[dict, str]:
task_data["interval"] = 60 * 60 * 24 # 默认检测间隔时间 1 天
if not (isinstance(task_data['cycle'], int) and task_data['cycle'] > 1):
return "The remaining time parameter is incorrect, at least 1 day"
return task_data
def filter_template(self, template) -> dict:
from .util import get_cert_list, to_dict_obj
items = [
{"title": "{} | {}".format(i["title"],",".join(i.get("domainName", []) or "None")), "value": i["ssl_id"]}
for i in get_cert_list(to_dict_obj({}))['data']
if i.get("endDay")
]
template["field"][0]["items"].extend(items)
return template
def check_num_rule(self, num_rule: dict) -> Union[dict, str]:
num_rule["get_by_func"] = "can_send_by_num_rule"
return num_rule
# 实际的次数检查已在 get_push_data 其他位置完成
def can_send_by_num_rule(self, task_id: str, task_data: dict, number_rule: dict, push_data: dict) -> Optional[str]:
return None
def task_run_end_hook(self, res) -> None:
if not res["do_send"]:
return
if self.task_id:
if self.task_id not in self.tips:
self.tips[self.task_id] = {}
for w in self.push_keys:
if w in self.tips[self.task_id]:
self.tips[self.task_id][w] += 1
else:
self.tips[self.task_id][w] = 1
self.save_tip()
def task_config_update_hook(self, task: dict) -> None:
if task["id"] in self.tips:
self.tips.pop(task["id"])
self.save_tip()
def task_config_remove_hook(self, task: dict) -> None:
if task["id"] in self.tips:
self.tips.pop(task["id"])
self.save_tip()
class ViewMsgFormat(object):
_FORMAT = {
"1": (
lambda x: "<span>Time remaining less than {} days {}</span>".format(
x["task_data"].get("cycle"),
("(If it is not processed, it will be resent 1 time the next day for %d days)" % x.get("number_rule", {}).get("total", 0)) if x.get("number_rule", {}).get("total", 0) else ""
)
)
}
def get_msg(self, task: dict) -> Optional[str]:
if task["template_id"] in ["70", "71"]:
return self._FORMAT["1"](task)
if task["template_id"] in self._FORMAT:
return self._FORMAT[task["template_id"]](task)
return None

View File

@@ -0,0 +1,127 @@
[
{
"id": "70",
"ver": "1",
"used": true,
"source": "domain_endtime",
"title": "Domain expiration",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.ssl_push",
"name": "DomainEndTimeTask"
},
"template": {
"field": [
{
"attr": "project",
"name": "domain",
"type": "select",
"default": "all",
"items": [
{
"title": "All domain",
"value": "all"
}
]
},
{
"attr": "cycle",
"name": "Remaining days",
"type": "number",
"suffix": "",
"unit": "day(s)",
"default": 30
}
],
"sorted": [
[
"project"
],
[
"cycle"
]
]
},
"default": {
"project": "all",
"cycle": 30
},
"advanced_default": {
"number_rule": {
"total": 2
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": false
},
{
"id": "71",
"ver": "1",
"used": true,
"source": "cert_endtime",
"title": "Certificate expiration",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.ssl_push",
"name": "CertEndTimeTask"
},
"template": {
"field": [
{
"attr": "project",
"name": "Certificate",
"type": "select",
"default": "all",
"items": [
{
"title": "all certificates",
"value": "all"
}
]
},
{
"attr": "cycle",
"name": "Remaining days",
"type": "number",
"suffix": "",
"unit": "day(s)",
"default": 30
}
],
"sorted": [
[
"project"
],
[
"cycle"
]
]
},
"default": {
"project": "all",
"cycle": 30
},
"advanced_default": {
"number_rule": {
"total": 2
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg"
],
"unique": false
}
]

491
mod/base/push_mod/system.py Normal file
View File

@@ -0,0 +1,491 @@
import datetime
import time
from threading import Thread
from typing import Optional, List, Dict, Type, Union, Any
import public
from .base_task import BaseTask
from .compatible import rsync_compatible
from .mods import TaskTemplateConfig, TaskConfig, TaskRecordConfig, SenderConfig
from .send_tool import sms_msg_normalize
from .tool import load_task_cls_by_path, load_task_cls_by_function, T_CLS
from .util import get_server_ip, get_network_ip, format_date, get_config_value
WAIT_TASK_LIST: List[Thread] = []
class PushSystem:
def __init__(self):
self.task_cls_cache: Dict[str, Type[T_CLS]] = {} # NOQA
self._today_zero: Optional[datetime.datetime] = None
self._sender_type_class: Optional[dict] = {}
self.sd_cfg = SenderConfig()
def sender_cls(self, sender_type: str):
if not self._sender_type_class:
from mod.base.msg import WeiXinMsg, MailMsg, WebHookMsg, FeiShuMsg, DingDingMsg, SMSMsg, TgMsg
self._sender_type_class = {
"weixin": WeiXinMsg,
"mail": MailMsg,
"webhook": WebHookMsg,
"feishu": FeiShuMsg,
"dingding": DingDingMsg,
"sms": SMSMsg,
# "wx_account": WeChatAccountMsg,
"tg": TgMsg,
}
return self._sender_type_class[sender_type]
@staticmethod
def remove_old_task(task: dict):
if not task.get("id"):
return
task_id = task["id"]
try:
from . import PushManager
PushManager().remove_task_conf(public.to_dict_obj(
{"task_id": task_id}
))
except:
pass
@staticmethod
def can_run_task_list():
result = []
result_template = {}
for task in TaskConfig().config: # all task
# ======== 移除旧任务 ==============
if task.get("source") == "cert_endtime" and task.get("task_data", {}).get(
"title") == "Certificate expiration":
PushSystem.remove_old_task(task) # 移除旧的ssl通知
# ======== 移除旧任务 End ==========
if not task["status"]:
continue
# 间隔检测时间未到跳过
if "interval" in task["task_data"] and isinstance(task["task_data"]["interval"], int):
if time.time() < task["last_check"] + task["task_data"]["interval"]:
continue
result.append(task)
for template in TaskTemplateConfig().config: # task's template
if template.get("id") == task["template_id"] and template.get("used"):
result_template.update({task["id"]: template})
break
return result, result_template
def get_task_object(self, template_id, load_cls_data: dict) -> Optional[BaseTask]:
if template_id in self.task_cls_cache:
return self.task_cls_cache[template_id]()
if "load_type" not in load_cls_data:
return None
if load_cls_data["load_type"] == "func":
cls = load_task_cls_by_function(
name=load_cls_data["name"],
func_name=load_cls_data["func_name"],
is_model=load_cls_data.get("is_model", False),
model_index=load_cls_data.get("is_model", ''),
args=load_cls_data.get("args", None),
sub_name=load_cls_data.get("sub_name", None),
)
else:
cls_path = load_cls_data["cls_path"]
cls = load_task_cls_by_path(cls_path, load_cls_data["name"])
if not cls:
return None
self.task_cls_cache[template_id] = cls
return cls()
def run(self):
rsync_compatible()
task_list, task_template = self.can_run_task_list()
try:
for t in task_list:
template = task_template[t["id"]]
print(PushRunner(t, template, self)())
except Exception as e:
import traceback
public.print_log(f"run task error %s", e)
public.print_log(traceback.format_exc())
global WAIT_TASK_LIST
if WAIT_TASK_LIST: # 有任务启用子线程的,要等到这个线程结束,再结束主线程
for i in WAIT_TASK_LIST:
i.join()
def get_today_zero(self) -> datetime.datetime:
if self._today_zero is None:
t = datetime.datetime.today()
t_zero = datetime.datetime.combine(t, datetime.time.min)
self._today_zero = t_zero
return self._today_zero
class PushRunner:
def __init__(self, task: dict, template: dict, push_system: PushSystem, custom_push_data: Optional[dict] = None):
self._public_push_data: Optional[dict] = None
self.result: dict = {
"do_send": False,
"stop_msg": "",
"push_data": {},
"check_res": False,
"check_stop_on": "",
"send_data": {},
} # 记录结果
self.change_fields = set() # 记录task变化值
self.task_obj: Optional[BaseTask] = None
self.task = task
self.template = template
self.push_system = push_system
self._add_hook_msg: Optional[str] = None # 记录前置钩子处理后的追加信息
self.custom_push_data = custom_push_data
self.tr_cfg = TaskRecordConfig(task["id"])
self.is_number_rule_by_func = False # 记录这个任务是否使用自定义的次数检测, 如果是,就不需要做次数更新
def save_result(self):
t = TaskConfig()
tmp = t.get_by_id(self.task["id"])
if tmp:
for f in self.change_fields:
tmp[f] = self.task[f]
if self.result["do_send"]:
tmp["last_send"] = int(time.time())
tmp["last_check"] = int(time.time())
t.save_config()
if self.result["push_data"]:
result_data = self.result.copy()
self.tr_cfg.config.append(
{
"id": self.tr_cfg.nwe_id(),
"template_id": self.template["id"],
"task_id": self.task["id"],
"do_send": result_data.pop("do_send"),
"send_data": result_data.pop("push_data"),
"result": result_data,
"create_time": int(time.time()),
}
)
self.tr_cfg.save_config()
@property
def public_push_data(self) -> dict:
if self._public_push_data is None:
self._public_push_data = {
'ip': get_server_ip(),
'local_ip': get_network_ip(),
'server_name': get_config_value('title')
}
data = self._public_push_data.copy()
data['time'] = format_date()
data['timestamp'] = int(time.time())
return data
def __call__(self):
self.run()
self.save_result()
if self.task_obj:
self.task_obj.task_run_end_hook(self.result)
return self.result_to_return()
def result_to_return(self) -> dict:
return self.result
def _append_msg_list_for_hook(self, push_data: dict) -> dict:
for key in ["pre_hook", "after_hook"]:
if not self.task.get("task_data", {}).get(key):
continue
for k, v in self.task["task_data"][key].items():
try:
val = ", ".join(v) if isinstance(v, list) else str(v)
act = k.capitalize() if k and isinstance(k , str) else k
push_data['msg_list'].append(f">{key.capitalize()}: {act} - {val} ")
except Exception as e:
public.print_log(f"Append {key} hook msg error: {e}")
continue
return push_data
def run(self):
self.task_obj = self.push_system.get_task_object(self.template["id"], self.template["load_cls"])
if not self.task_obj:
self.result["stop_msg"] = "The task class failed to load"
return
if self.custom_push_data is None:
push_data = None
try:
push_data = self.task_obj.get_push_data(self.task["id"], self.task["task_data"])
except Exception:
import traceback
public.print_log(f"get_push_data error: {traceback.format_exc()}")
if not push_data:
return
else:
push_data = self.custom_push_data
self.result["push_data"] = push_data
# 执行全局前置钩子
if self.task.get("pre_hook"):
if not self.run_hook(self.task["pre_hook"], "pre_hook"):
self.result["stop_msg"] = "Task global pre hook stopped execution"
return
# 执行任务自身前置钩子
if self.task.get("task_data", {}).get("pre_hook"):
if not self.run_hook(self.task["task_data"]["pre_hook"], "pre_hook"):
self.result["stop_msg"] = "Task pre hook stopped execution"
return
# 执行时间规则判断
if not self.run_time_rule(self.task["time_rule"]):
return
# 执行频率规则判断
if not self.number_rule(self.task["number_rule"]):
return
# 注入任务自身更多钩子消息, 全局钩子静默处理
push_data = self._append_msg_list_for_hook(push_data)
# 执行发送信息
self.send_message(push_data)
self.change_fields.add("number_data")
if "day_num" not in self.task["number_data"]:
self.task["number_data"]["day_num"] = 0
if "total" not in self.task["number_data"]:
self.task["number_data"]["total"] = 0
self.task["number_data"]["day_num"] += 1
self.task["number_data"]["total"] += 1
self.task["number_data"]["time"] = int(time.time())
# 执行任务自身后置钩子
if self.task.get("task_data", {}).get("after_hook"):
self.run_hook(self.task["task_data"]["after_hook"], "after_hook")
# 执行全局后置钩子
if self.task.get("after_hook"):
self.run_hook(self.task["after_hook"], "after_hook")
# hook函数, 额外扩展
def run_hook(self, hook_data: Dict[str, List[Any]], hook_name: str) -> bool:
"""
执行hook操作并返回是否继续执行, 并将hook的执行结果记录
@param hook_name: 钩子的名称after_hook pre_hook
@param hook_data: 执行的内容
@return: bool
"""
if not isinstance(hook_data, dict) or not isinstance(hook_name, str):
return False
if hook_name == "pre_hook":
return True
elif hook_name == "after_hook":
from script.restart_services import ServicesHelper
# restart action
if hook_data.get("restart"):
for s in hook_data.get("restart", []):
if not s or not isinstance(s, str):
continue
service_obj = ServicesHelper(s.strip())
if not service_obj.is_install:
continue
service_obj.script("restart", "Alarm Triggered")
return True
# module action
elif hook_data.get("module"):
return True
return False
def run_time_rule(self, time_rule: dict) -> bool:
if "send_interval" in time_rule and time_rule["send_interval"] > 0:
if self.task["last_send"] + time_rule["send_interval"] > time.time():
self.result['stop_msg'] = 'If the minimum send time is less, no sending will be made'
self.result['check_stop_on'] = "time_rule_send_interval"
return False
time_range = time_rule.get("time_range", None)
if time_range and isinstance(time_range, list) and len(time_range) == 2:
t_zero = self.push_system.get_today_zero()
start_time = t_zero + datetime.timedelta(seconds=time_range[0]) # NOQA
end_time = t_zero + datetime.timedelta(seconds=time_range[1]) # NOQA
if not start_time < datetime.datetime.now() < end_time:
self.result['stop_msg'] = 'It is not within the time frame within which the alarm can be sent'
self.result['check_stop_on'] = "time_rule_time_range"
return False
return True
def number_rule(self, number_rule: dict) -> bool:
number_data = self.task.get("number_data", {})
# 判断通过 自定义函数的方式确认是否达到发送次数
if "get_by_func" in number_rule and isinstance(number_rule["get_by_func"], str):
f = getattr(self.task_obj, number_rule["get_by_func"], None)
if f is not None and callable(f):
res = f(self.task["id"], self.task["task_data"], number_data, self.result["push_data"])
if isinstance(res, str):
self.result['stop_msg'] = res
self.result['check_stop_on'] = "number_rule_get_by_func"
return False
# 只要是走了使用函数检查的,不再处理默认情况 change_fields 中不添加 number_data
return True
if "day_num" in number_rule and isinstance(number_rule["day_num"], int) and number_rule["day_num"] > 0:
record_time = number_data.get("time", 0)
if record_time < self.push_system.get_today_zero().timestamp(): # 昨日触发
self.task["number_data"]["day_num"] = record_num = 0
self.task["number_data"]["time"] = time.time()
self.change_fields.add("number_data")
else:
record_num = self.task["number_data"].get("day_num")
if record_num >= number_rule["day_num"]:
self.result['stop_msg'] = "Exceeding the daily limit:{}".format(number_rule["day_num"])
self.result['check_stop_on'] = "number_rule_day_num"
return False
if "total" in number_rule and isinstance(number_rule["total"], int) and number_rule["total"] > 0:
record_total = number_data.get("total", 0)
if record_total >= number_rule["total"]:
self.result['stop_msg'] = "The maximum number of times the limit is exceeded:{}".format(
number_rule["total"])
self.result['check_stop_on'] = "number_rule_total"
return False
return True
def send_message(self, push_data: dict):
self.result["do_send"] = True
self.result["push_data"] = push_data
for sender_id in self.task["sender"]:
conf = self.push_system.sd_cfg.get_by_id(sender_id)
if conf is None:
continue
if not conf["used"]:
self.result["send_data"][sender_id] = "The alarm channel {} is closed, skip sending".format(
conf["data"].get("title"))
continue
sd_cls = self.push_system.sender_cls(conf["sender_type"])
res = None
if conf["sender_type"] == "weixin":
res = sd_cls(conf).send_msg(
self.task_obj.to_weixin_msg(push_data, self.public_push_data),
self.task_obj.title
)
elif conf["sender_type"] == "mail":
res = sd_cls(conf).send_msg(
self.task_obj.to_mail_msg(push_data, self.public_push_data),
self.task_obj.title
)
elif conf["sender_type"] == "webhook":
res = sd_cls(conf).send_msg(
self.task_obj.to_web_hook_msg(push_data, self.public_push_data),
self.task_obj.title,
)
elif conf["sender_type"] == "feishu":
res = sd_cls(conf).send_msg(
self.task_obj.to_feishu_msg(push_data, self.public_push_data),
self.task_obj.title
)
elif conf["sender_type"] == "dingding":
res = sd_cls(conf).send_msg(
self.task_obj.to_dingding_msg(push_data, self.public_push_data),
self.task_obj.title
)
elif conf["sender_type"] == "sms":
sm_type, sm_args = self.task_obj.to_sms_msg(push_data, self.public_push_data)
if not sm_type or not sm_args:
continue
sm_args = sms_msg_normalize(sm_args)
res = sd_cls(conf).send_msg(sm_type, sm_args)
elif conf["sender_type"] == "tg":
# public.print_log("tg -- 发送数据 {}".format(self.task_obj.to_tg_msg(push_data, self.public_push_data)))
from mod.base.msg import TgMsg
# Home CPU alarms<br>
# >Server:xxx<br>
# >IPAddress: xxx.xxx.xxx.xxx(Internet) xxx.xxx.xxx.xxx(Internal)<br>
# >SendingTime: 2024-00-00 00:00:00<br>
# >Notification type: High CPU usage alarm<br>
# >Content of alarm: The average CPU usage of the machine in the last 5 minutes is 3.24%, which is higher than the alarm value 1%.
try:
res = sd_cls(conf).send_msg(
# res = TgMsg().send_msg(
self.task_obj.to_tg_msg(push_data, self.public_push_data),
self.task_obj.title
)
except:
public.print_log(public.get_error_info())
else:
continue
if isinstance(res, str) and res.find("Traceback") != -1:
self.result["send_data"][sender_id] = ("An error occurred during the execution of the message "
"transmission, and the transmission was not successful")
if isinstance(res, str):
self.result["send_data"][sender_id] = res
else:
self.result["send_data"][sender_id] = 1
def push_by_task_keyword(source: str, keyword: str, push_data: Optional[dict] = None) -> Union[str, dict]:
"""
通过关键字查询告警任务,并发送信息
@param push_data:
@param source:
@type keyword:
@return:
"""
push_system = PushSystem()
target_task = {}
for i in TaskConfig().config:
if i["source"] == source and i["keyword"] == keyword:
target_task = i
break
if not target_task:
return "The task was not found"
target_template = TaskTemplateConfig().get_by_id(target_task["template_id"]) # NOQA
if not target_template["used"]:
return "This task type has been banned"
if not target_task['status']:
return "The task has been stopped"
return PushRunner(target_task, target_template, push_system, push_data)()
def push_by_task_id(task_id: str, push_data: Optional[dict] = None):
"""
通过任务id触发告警 并 发送信息
@param push_data:
@param task_id:
@return:
"""
push_system = PushSystem()
target_task = TaskConfig().get_by_id(task_id)
if not target_task:
return "The task was not found"
target_template = TaskTemplateConfig().get_by_id(target_task["template_id"])
if not target_template["used"]:
return "This task type has been banned"
if not target_task['status']:
return "The task has been stopped"
return PushRunner(target_task, target_template, push_system, push_data)()
def get_push_public_data():
data = {
'ip': get_server_ip(),
'local_ip': get_network_ip(),
'server_name': get_config_value('title'),
'time': format_date(),
'timestamp': int(time.time())}
return data

View File

@@ -0,0 +1,418 @@
import json
import os
import sys
import threading
import time
from datetime import datetime, timedelta
from importlib import import_module
from typing import Tuple, Union, Optional, List
import psutil
from .base_task import BaseTask
from .mods import PUSH_DATA_PATH
from .send_tool import WxAccountMsg
from .system import WAIT_TASK_LIST
from .util import read_file, write_file, get_config_value, generate_fields
try:
if "/www/server/panel/class" not in sys.path:
sys.path.insert(0, "/www/server/panel/class")
from panel_msg.collector import SitePushMsgCollect, SystemPushMsgCollect
except ImportError:
SitePushMsgCollect = None
SystemPushMsgCollect = None
def _get_panel_name() -> str:
data = get_config_value("title") # 若获得别名,则使用别名
if data == "":
data = "YakPanel"
return data
class PanelSysDiskTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "system_disk"
self.template_name = "Home disk alerts"
self.title = "Home disk alerts"
self.wx_msg = ""
def get_title(self, task_data: dict) -> str:
return "Home disk alerts -- Mount directory[{}]".format(task_data["project"])
def check_task_data(self, task_data: dict) -> Union[dict, str]:
if task_data["project"] not in [i[0] for i in self._get_disk_name()]:
return "The specified disk does not exist"
if not (isinstance(task_data['cycle'], int) and task_data['cycle'] in (1, 2)):
return "The type parameter is incorrect"
if not (isinstance(task_data['count'], int) and task_data['count'] >= 1):
return "The threshold parameter is incorrect"
if task_data['cycle'] == 2 and task_data['count'] >= 100:
return "The threshold parameter is incorrect, and the set check range is incorrect"
task_data['interval'] = 600
return task_data
@staticmethod
def _get_disk_name() -> list:
"""获取硬盘挂载点"""
if "/www/server/panel" not in sys.path:
sys.path.insert(0, "/www/server/panel")
system_modul = import_module('.system', package="class")
system = getattr(system_modul, "system")
disk_info = system.GetDiskInfo2(None, human=False)
return [(d.get("path"), d.get("size")[0]) for d in disk_info]
@staticmethod
def _get_disk_info() -> list:
"""获取硬盘挂载点"""
if "/www/server/panel" not in sys.path:
sys.path.insert(0, "/www/server/panel")
system_modul = import_module('.system', package="class")
system = getattr(system_modul, "system")
disk_info = system.GetDiskInfo2(None, human=False)
return disk_info
def get_keyword(self, task_data: dict) -> str:
return task_data["project"]
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
disk_info = self._get_disk_info()
unsafe_disk_list = []
for d in disk_info:
if task_data["project"] != d["path"]:
continue
free = int(d["size"][2]) / 1048576
proportion = int(d["size"][3] if d["size"][3][-1] != "%" else d["size"][3][:-1])
if task_data["cycle"] == 1 and free < task_data["count"]:
unsafe_disk_list.append(
"The remaining capacity of the disk mounted on {} is {}G, which is less than the alarm value {}G.".format(
d["path"], round(free, 2), task_data["count"])
)
self.wx_msg = "The remaining capacity is less than {}G".format(task_data["count"])
elif task_data["cycle"] == 2 and proportion > task_data["count"]:
unsafe_disk_list.append(
"The used capacity of the disk mounted on {} is {}%, which is greater than the alarm value {}%.".format(
d["path"], round(proportion, 2), task_data["count"])
)
self.wx_msg = "Occupancy greater than {}%".format(task_data["count"])
if len(unsafe_disk_list) == 0:
return None
return {
"msg_list": [
">Notification type: Disk Balance Alert",
">Alarm content:\n" + "\n".join(unsafe_disk_list)
]
}
def filter_template(self, template: dict) -> Optional[dict]:
for (path, total_size) in self._get_disk_name():
template["field"][0]["items"].append({
"title": "[{}] disk".format(path),
"value": path,
"count_default": round((int(total_size) * 0.2) / 1024 / 1024, 1)
})
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return 'machine_exception|Disk Balance Alert', {
'name': _get_panel_name(),
'type': "Insufficient disk space",
}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "Home disk alerts"
if len(self.wx_msg) > 20:
self.wx_msg = self.wx_msg[:17] + "..."
msg.msg = self.wx_msg
return msg
class PanelSysCPUTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "system_cpu"
self.template_name = "Home CPU alarms"
self.title = "Home CPU alarms"
self.cpu_count = 0
self._tip_file = "{}/system_cpu.tip".format(PUSH_DATA_PATH)
self._tip_data: Optional[List[Tuple[float, float]]] = None
@property
def cache_list(self) -> List[Tuple[float, float]]:
if self._tip_data is not None:
return self._tip_data
try:
self._tip_data = json.loads(read_file(self._tip_file))
except:
self._tip_data = []
return self._tip_data
def save_cache_list(self):
write_file(self._tip_file, json.dumps(self.cache_list))
def check_task_data(self, task_data: dict) -> Union[dict, str]:
if not (isinstance(task_data['cycle'], int) and task_data['cycle'] >= 1):
return "The time parameter is incorrect"
if not (isinstance(task_data['count'], int) and task_data['count'] >= 1):
return "Threshold parameter error, at least 1%"
task_data['interval'] = 60
return task_data
def get_keyword(self, task_data: dict) -> str:
return "system_cpu"
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
expiration = datetime.now() - timedelta(seconds=task_data["cycle"] * 60 + 10)
for i in range(len(self.cache_list) - 1, -1, -1):
data_time, _ = self.cache_list[i]
if datetime.fromtimestamp(data_time) < expiration:
del self.cache_list[i]
# 记录下次的
def thread_get_cpu_data():
self.cache_list.append((time.time(), psutil.cpu_percent(10)))
self.save_cache_list()
thread_active = threading.Thread(target=thread_get_cpu_data, args=())
thread_active.start()
WAIT_TASK_LIST.append(thread_active)
if len(self.cache_list) < task_data["cycle"]: # 小于指定次数不推送
return None
if len(self.cache_list) > 0:
avg_data = sum(i[1] for i in self.cache_list) / len(self.cache_list)
else:
avg_data = 0
if avg_data < task_data["count"]:
return None
else:
self.cache_list.clear()
self.cpu_count = round(avg_data, 2)
s_list = [
">Notification type: High CPU usage alarm",
">Content of alarm: The average CPU usage of the machine in the last {} minutes is {}%, which is higher than the alarm value {}%.".format(
task_data["cycle"], round(avg_data, 2), task_data["count"]),
]
return {
"msg_list": s_list,
}
def filter_template(self, template: dict) -> Optional[dict]:
template = generate_fields(template, "restart")
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return 'machine_exception|High CPU usage alarm', {
'name': _get_panel_name(),
'type': "High CPU usage",
}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "Home CPU alarms"
msg.msg = "The CPU usage of the host is exceeded:{}%".format(self.cpu_count)
msg.next_msg = "Please log in to the panel to view the host status"
return msg
class PanelSysLoadTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "system_load"
self.template_name = "Home load alerts"
self.title = "Home load alerts"
self.avg_data = 0
def check_task_data(self, task_data: dict) -> Union[dict, str]:
if not (isinstance(task_data['cycle'], int) and task_data['cycle'] >= 1):
return "The time parameter is incorrect"
if not (isinstance(task_data['count'], int) and task_data['count'] >= 1):
return "Threshold parameter error, at least 1%"
task_data['interval'] = 60 * task_data['cycle']
return task_data
def get_keyword(self, task_data: dict) -> str:
return "system_load"
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
now_load = os.getloadavg()
cpu_count = psutil.cpu_count()
now_load = [i / (cpu_count * 2) * 100 for i in now_load]
need_push = False
avg_data = 0
if task_data["cycle"] == 15 and task_data["count"] < now_load[2]:
avg_data = now_load[2]
need_push = True
elif task_data["cycle"] == 5 and task_data["count"] < now_load[1]:
avg_data = now_load[1]
need_push = True
elif task_data["cycle"] == 1 and task_data["count"] < now_load[0]:
avg_data = now_load[0]
need_push = True
if not need_push:
return None
self.avg_data = avg_data
return {
"msg_list": [
">Notification type: Alarm when the load exceeds the standard",
">Content of alarm: The average load factor of the machine in the last {} minutes is {}%, which is higher than the alarm value of {}%.".format(
task_data["cycle"], round(avg_data, 2), task_data["count"]),
]
}
def filter_template(self, template: dict) -> Optional[dict]:
template = generate_fields(template, "restart")
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return 'machine_exception|Alarm when the load exceeds the standard', {
'name': _get_panel_name(),
'type': "The average load is too high",
}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "Home load alerts"
msg.msg = "The host load exceeds:{}%".format(round(self.avg_data, 2))
msg.next_msg = "Please log in to the panel to view the host status"
return msg
class PanelSysMEMTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "system_mem"
self.template_name = "Home memory alarms"
self.title = "Home memory alarms"
self.wx_data = 0
self._tip_file = "{}/system_mem.tip".format(PUSH_DATA_PATH)
self._tip_data: Optional[List[Tuple[float, float]]] = None
@property
def cache_list(self) -> List[Tuple[float, float]]:
if self._tip_data is not None:
return self._tip_data
try:
self._tip_data = json.loads(read_file(self._tip_file))
except:
self._tip_data = []
return self._tip_data
def save_cache_list(self):
write_file(self._tip_file, json.dumps(self.cache_list))
def check_task_data(self, task_data: dict) -> Union[dict, str]:
if not (isinstance(task_data['cycle'], int) and task_data['cycle'] >= 1):
return "The number parameter is incorrect"
if not (isinstance(task_data['count'], int) and task_data['count'] >= 1):
return "Threshold parameter error, at least 1%"
task_data['interval'] = task_data['cycle'] * 60
return task_data
def get_keyword(self, task_data: dict) -> str:
return "system_mem"
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
mem = psutil.virtual_memory()
real_used: float = (mem.total - mem.free - mem.buffers - mem.cached) / mem.total
stime = datetime.now()
expiration = stime - timedelta(seconds=task_data["cycle"] * 60 + 10)
self.cache_list.append((stime.timestamp(), real_used))
for i in range(len(self.cache_list) - 1, -1, -1):
data_time, _ = self.cache_list[i]
if datetime.fromtimestamp(data_time) < expiration:
del self.cache_list[i]
avg_data = sum(i[1] for i in self.cache_list) / len(self.cache_list)
if avg_data * 100 < task_data["count"]:
self.save_cache_list()
return None
else:
self.cache_list.clear()
self.save_cache_list()
self.wx_data = round(avg_data * 100, 2)
return {
'msg_list': [
">Notification type: High memory usage alarm",
">Content of alarm: The average memory usage of the machine in the last {} minutes is {}%, which is higher than the alarm value {}%.".format(
task_data["cycle"], round(avg_data * 100, 2), task_data["count"]),
]
}
def filter_template(self, template: dict) -> Optional[dict]:
template = generate_fields(template, "restart")
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return 'machine_exception|High memory usage alarm', {
'name': _get_panel_name(),
'type': "High memory usage",
}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "Home memory alarms"
msg.msg = "Host memory usage exceeded: {}%".format(self.wx_data)
msg.next_msg = "Please log in to the panel to view the host status"
return msg
class ViewMsgFormat(object):
_FORMAT = {
"20": (
lambda x: "<span>Triggered by {} disk mounted on {}</span>".format(
x.get("project"),
"The margin is less than %.1f G" % round(x.get("count"), 1) if x.get(
"cycle") == 1 else "ake up more than %d%%" % x.get("count"),
)
),
"21": (
lambda x: "<span>Triggers when the average CPU usage exceeds {}% in {} minutes</span>".format(
x.get("count"), x.get("cycle")
)
),
"22": (
lambda x: "<span>Triggered by an average load exceeding {}% in {} minutes</span>".format(
x.get("count"), x.get("cycle")
)
),
"23": (
lambda x: "<span>Triggered if the memory usage exceeds {}% within {} minutes</span>".format(
x.get("count"), x.get("cycle")
)
)
}
def get_msg(self, task: dict) -> Optional[str]:
if task["template_id"] in self._FORMAT:
return self._FORMAT[task["template_id"]](task["task_data"])
return None

View File

@@ -0,0 +1,294 @@
[
{
"id": "20",
"ver": "1",
"used": true,
"source": "system_disk",
"title": "Home disk alerts",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.system_push",
"name": "PanelSysDiskTask"
},
"template": {
"field": [
{
"attr": "project",
"name": "disk information",
"type": "select",
"items": [
]
},
{
"attr": "cycle",
"name": "detection type",
"type": "radio",
"suffix": "",
"default": 2,
"items": [
{
"title": "Remaining capacity",
"value": 1
},
{
"title": "percentage occupied",
"value": 2
}
]
},
{
"attr": "count",
"name": "occupancy rate exceeds",
"type": "number",
"unit": "%",
"suffix": "will trigger an alarm",
"default": 80,
"err_msg_prefix": "disk threshold"
}
],
"sorted": [
[
"project"
],
[
"cycle"
],
[
"count"
]
]
},
"default": {
"project": "/",
"cycle": 2,
"count": 80
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"sms",
"tg"
],
"unique": false
},
{
"id": "21",
"ver": "1",
"used": true,
"source": "system_cpu",
"title": "Home CPU alarms",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.system_push",
"name": "PanelSysCPUTask"
},
"template": {
"field": [
{
"attr": "cycle",
"name": "every time",
"type": "select",
"unit": "minute(s)",
"suffix": "average ",
"width": "70px",
"disabled": true,
"default": 5,
"items": [
{
"title": "1",
"value": 3
},
{
"title": "5",
"value": 5
},
{
"title": "15",
"value": 15
}
]
},
{
"attr": "count",
"name": "CPU usage exceeded",
"type": "number",
"unit": "%",
"suffix": "will trigger an alarm",
"default": 80,
"err_msg_prefix": "CPU"
}
],
"sorted": [
[
"cycle"
],
[
"count"
]
]
},
"default": {
"cycle": 5,
"count": 80
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"sms",
"tg"
],
"unique": true
},
{
"id": "22",
"ver": "1",
"used": true,
"source": "system_load",
"title": "Home load alerts",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.system_push",
"name": "PanelSysLoadTask"
},
"template": {
"field": [
{
"attr": "cycle",
"name": "every time",
"type": "select",
"unit": "minute(s)",
"suffix": "average ",
"default": 5,
"width": "70px",
"disabled": true,
"items": [
{
"title": "1",
"value": 1
},
{
"title": "5",
"value": 5
},
{
"title": "15",
"value": 15
}
]
},
{
"attr": "count",
"name": " load over",
"type": "number",
"unit": "%",
"suffix": "will trigger an alarm",
"default": 80,
"err_msg_prefix": "Load"
}
],
"sorted": [
[
"cycle"
],
[
"count"
]
]
},
"default": {
"cycle": 5,
"count": 80
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg",
"sms"
],
"unique": true
},
{
"id": "23",
"ver": "1",
"used": true,
"source": "system_mem",
"title": "Home memory alarms",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.system_push",
"name": "PanelSysMEMTask"
},
"template": {
"field": [
{
"attr": "cycle",
"name": "every time",
"type": "select",
"unit": "minute(s)",
"suffix": "average ",
"width": "70px",
"disabled": true,
"default": 5,
"items": [
{
"title": "1",
"value": 3
},
{
"title": "5",
"value": 5
},
{
"title": "15",
"value": 15
}
]
},
{
"attr": "count",
"name": "Memory usage is more than",
"type": "number",
"unit": "%",
"suffix": "will trigger an alarm",
"default": 80,
"err_msg_prefix": "Memory"
}
],
"sorted": [
[
"cycle"
],
[
"count"
]
]
},
"default": {
"cycle": 5,
"count": 80
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg",
"sms"
],
"unique": true
}
]

View File

@@ -0,0 +1,506 @@
import json
import os
import sys
import threading
import time
from datetime import datetime, timedelta
from importlib import import_module
from typing import Tuple, Union, Optional, List
import psutil
from .send_tool import WxAccountMsg
from .base_task import BaseTask
from .mods import PUSH_DATA_PATH, TaskTemplateConfig
from .util import read_file, write_file, get_config_value, GET_CLASS
class _ProcessInfo:
def __init__(self):
self.data = None
self.last_time = 0
def __call__(self) -> list:
if self.data is not None and time.time() - self.last_time < 60:
return self.data
try:
import PluginLoader
get_obj = GET_CLASS()
get_obj.sort = "status"
p_info = PluginLoader.plugin_run("task_manager", "get_process_list", get_obj)
except:
return []
if isinstance(p_info, dict) and "process_list" in p_info and isinstance(
p_info["process_list"], list):
self._process_info = p_info["process_list"]
self.last_time = time.time()
return self._process_info
else:
return []
get_process_info = _ProcessInfo()
def have_task_manager_plugin():
"""
通过文件判断是否有进程管理器
"""
return os.path.exists("/www/server/panel/plugin/task_manager/task_manager_push.py")
def load_task_manager_template():
if TaskTemplateConfig().get_by_id("60"):
return None
from .mods import load_task_template_by_config
load_task_template_by_config([
{
"id": "60",
"ver": "1",
"used": True,
"source": "task_manager_cpu",
"title": "Task Manager CPU usage alarm",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.task_manager_push",
"name": "TaskManagerCPUTask"
},
"template": {
"field": [
{
"attr": "project",
"name": "project name",
"type": "select",
"items": {
"url": "plugin?action=a&name=task_manager&s=get_process_list_to_push"
}
},
{
"attr": "count",
"name": "Occupancy exceeded",
"type": "number",
"unit": "%",
"suffix": "trigger an alarm",
"default": 80,
"err_msg_prefix": "CPU occupancy"
},
{
"attr": "interval",
"name": "Interval",
"type": "number",
"unit": "second(s)",
"suffix": "monitor the detection conditions again",
"default": 600
}
],
"sorted": [
[
"project"
],
[
"count"
],
[
"interval"
]
],
},
"default": {
"project": '',
"count": 80,
"interval": 600
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg",
],
"unique": False
},
{
"id": "61",
"ver": "1",
"used": True,
"source": "task_manager_mem",
"title": "Task Manager memory usage alarm",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.task_manager_push",
"name": "TaskManagerMEMTask"
},
"template": {
"field": [
{
"attr": "project",
"name": "project name",
"type": "select",
"items": {
"url": "plugin?action=a&name=task_manager&s=get_process_list_to_push"
}
},
{
"attr": "count",
"name": "The occupancy is more than",
"type": "number",
"unit": "MB",
"suffix": "trigger an alarm",
"default": None,
"err_msg_prefix": "Occupancy"
},
{
"attr": "interval",
"name": "Interval",
"type": "number",
"unit": "second(s)",
"suffix": "monitor the detection conditions again",
"default": 600
}
],
"sorted": [
[
"project"
],
[
"count"
],
[
"interval"
]
],
},
"default": {
"project": '',
"count": 80,
"interval": 600
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg",
],
"unique": False
},
{
"id": "62",
"ver": "1",
"used": True,
"source": "task_manager_process",
"title": "Task Manager Process Overhead Alert",
"load_cls": {
"load_type": "path",
"cls_path": "mod.base.push_mod.task_manager_push",
"name": "TaskManagerProcessTask"
},
"template": {
"field": [
{
"attr": "project",
"name": "project name",
"type": "select",
"items": {
"url": "plugin?action=a&name=task_manager&s=get_process_list_to_push"
}
},
{
"attr": "count",
"name": "Number of processes exceeds",
"type": "number",
"unit": "of them",
"suffix": "trigger an alarm",
"default": 20,
"err_msg_prefix": "NumberOfProcesses"
},
{
"attr": "interval",
"name": "Interval",
"type": "number",
"unit": "second(s)",
"suffix": "monitor the detection conditions again",
"default": 600
}
],
"sorted": [
[
"project"
],
[
"count"
],
[
"interval"
]
],
},
"default": {
"project": '',
"count": 80,
"interval": 600
},
"advanced_default": {
"number_rule": {
"day_num": 3
}
},
"send_type_list": [
"dingding",
"feishu",
"mail",
"weixin",
"webhook",
"tg",
],
"unique": False
}
])
class TaskManagerCPUTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "task_manager_cpu"
self.template_name = "Task Manager CPU usage alarm"
# self.title = "Task Manager CPU usage alarm"
def get_title(self, task_data: dict) -> str:
return "Task Manager CPU usage alarm -- [{}]".format(task_data["project"])
def check_task_data(self, task_data: dict) -> Union[dict, str]:
if "interval" not in task_data or not isinstance(task_data["interval"], int):
task_data["interval"] = 600
if task_data["interval"] < 60:
task_data["interval"] = 60
if "count" not in task_data or not isinstance(task_data["count"], int):
return "The check range is set incorrectly"
if not 1 <= task_data["count"] < 100:
return "The check range is set incorrectly"
if not task_data["project"]:
return "Please select a process"
return task_data
def get_keyword(self, task_data: dict) -> str:
return task_data["project"]
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
process_info = get_process_info()
self.title = self.get_title(task_data)
count = used = 0
for p in process_info:
if p["name"] == task_data['project']:
used += p["cpu_percent"]
count += 1 if "children" not in p else len(p["children"]) + 1
if used <= task_data['count']:
return None
return {
'msg_list':
[
">Notification type: Task Manager CPU usage alarm",
">Alarm content: There are {} processes with the process name [{}], and the proportion of CPU resources consumed is {}%, which is greater than the alarm threshold {}%.".format(
task_data['project'], count, used, task_data['count']
)
],
"project": task_data['project'],
"count": int(task_data['count'])
}
def filter_template(self, template: dict) -> Optional[dict]:
if not have_task_manager_plugin():
return None
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return '', {}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "Task Manager CPU usage alarm"
if len(push_data["project"]) > 11:
project = push_data["project"][:9] + ".."
else:
project = push_data["project"]
msg.msg = "The CPU of {} exceeds {}%".format(project, push_data["count"])
return msg
class TaskManagerMEMTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "task_manager_mem"
self.template_name = "Task Manager memory usage alarm"
# self.title = "Task Manager memory usage alarm"
def get_title(self, task_data: dict) -> str:
return "Task Manager memory usage alarm -- [{}].".format(task_data["project"])
def check_task_data(self, task_data: dict) -> Union[dict, str]:
if not task_data["project"]:
return "Please select a process"
if "interval" not in task_data or not isinstance(task_data["interval"], int):
task_data["interval"] = 600
task_data["interval"] = max(60, task_data["interval"])
if "count" not in task_data or not isinstance(task_data["count"], int):
return "The check range is set incorrectly"
if task_data["count"] < 1:
return "The check range is set incorrectly"
return task_data
def get_keyword(self, task_data: dict) -> str:
return task_data["project"]
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
process_info = get_process_info()
self.title = self.get_title(task_data)
used = count = 0
for p in process_info:
if p["name"] == task_data['project']:
used += p["memory_used"]
count += 1 if "children" not in p else len(p["children"]) + 1
if used <= task_data['count'] * 1024 * 1024:
return None
return {
'msg_list': [
">Notification type: Task Manager memory usage alarm",
">Alarm content: There are {} processes with process name [{}], and the memory resources consumed are {}MB, which is greater than the alarm threshold {}MB.".format(
task_data['project'], count, int(used / 1024 / 1024), task_data['count']
)
],
"project": task_data['project']
}
def filter_template(self, template: dict) -> Optional[dict]:
if not have_task_manager_plugin():
return None
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return '', {}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
if len(push_data["project"]) > 11:
project = push_data["project"][:9] + ".."
else:
project = push_data["project"]
msg.thing_type = "Task Manager memory usage alarm"
msg.msg = "The memory of {} exceeds the alarm value".format(project)
return msg
class TaskManagerProcessTask(BaseTask):
def __init__(self):
super().__init__()
self.source_name = "task_manager_process"
self.template_name = "Task Manager Process Overhead Alert"
self.title = "Task Manager Process Overhead Alert"
def get_title(self, task_data: dict) -> str:
return "Task Manager Process Overhead Alert [{}]".format(task_data["project"])
def check_task_data(self, task_data: dict) -> Union[dict, str]:
if not task_data["project"]:
return "Please select a process"
if "interval" not in task_data or not isinstance(task_data["interval"], int):
task_data["interval"] = 600
task_data["interval"] = max(60, task_data["interval"])
if "count" not in task_data or not isinstance(task_data["count"], int):
return "The check range is set incorrectly"
if task_data["count"] < 1:
return "The check range is set incorrectly"
return task_data
def get_keyword(self, task_data: dict) -> str:
return task_data["project"]
def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]:
process_info = get_process_info()
count = 0
for p in process_info:
if p["name"] == task_data['project']:
count += 1 if "children" not in p else len(p["children"]) + 1
if count <= task_data['count']:
return None
return {
'msg_list':
[
">Notification type: Task Manager Process Overhead Alert",
">Alarm content: There are {} processes with process name {}, which is greater than the alarm threshold.".format(
task_data['project'], count, task_data['count']
)
],
"project": task_data['project'],
"count": task_data['count'],
}
def filter_template(self, template: dict) -> Optional[dict]:
if not have_task_manager_plugin():
return None
return template
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
return '', {}
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = "Task Manager Process Overhead Alert"
if len(push_data["project"]) > 11:
project = push_data["project"][:9] + ".."
else:
project = push_data["project"]
if push_data["count"] > 100: # 节省字数
push_data["count"] = "LIMIT"
msg.msg = "{} has more children than {}".format(project, push_data["count"])
return msg
class ViewMsgFormat(object):
_FORMAT = {
"60": (
lambda x: "<span>Process: The CPU occupation of {} is more than {}% triggered</span>".format(
x.get("project"), x.get("count")
)
),
"61": (
lambda x: "<span>Process: Triggered when the memory usage of {} exceeds {}MB</span>".format(
x.get("project"), x.get("count")
)
),
"62": (
lambda x: "<span>Process: Triggered when the number of child processes exceeds {}</span>".format(
x.get("project"), x.get("count")
)
),
}
def get_msg(self, task: dict) -> Optional[str]:
if task["template_id"] in self._FORMAT:
return self._FORMAT[task["template_id"]](task["task_data"])
return None

92
mod/base/push_mod/tool.py Normal file
View File

@@ -0,0 +1,92 @@
import os
import sys
from typing import Optional, Type, TypeVar
import traceback
from importlib import import_module
from .base_task import BaseTask
from .util import GET_CLASS, get_client_ip, debug_log
T_CLS = TypeVar('T_CLS', bound=BaseTask)
def load_task_cls_by_function(
name: str,
func_name: str,
is_model: bool = False,
model_index: str = '',
args: Optional[dict] = None,
sub_name: Optional[str] = None,
) -> Optional[Type[T_CLS]]:
"""
从执行函数的结果中获取任务类
@param model_index: 模块来源例如新场景就是mod
@param name: 名称
@param func_name: 函数名称
@param is_model: 是否在Model中不在Model中就应该在插件中
@param args: 请求这个接口的参数, 默认为空
@param sub_name: 自分类名称, 如果有则会和主名称name做拼接
@return: 返回None 或者有效的任务类
"""
import PluginLoader
real_name = name
if isinstance(sub_name, str):
real_name = "{}/{}".format(name, sub_name)
get_obj = GET_CLASS()
if args is not None and isinstance(args, dict):
for key, value in args.items():
setattr(get_obj, key, value)
try:
if is_model:
get_obj.model_index = model_index
res = PluginLoader.module_run(real_name, func_name, get_obj)
else:
get_obj.fun = func_name
get_obj.s = func_name
get_obj.client_ip = get_client_ip
res = PluginLoader.plugin_run(name, func_name, get_obj)
except:
debug_log(traceback.format_exc())
return None
if isinstance(res, dict):
return None
elif isinstance(res, BaseTask):
return res.__class__
elif issubclass(res, BaseTask):
return res
return None
def load_task_cls_by_path(path: str, cls_name: str) -> Optional[Type[T_CLS]]:
try:
# 插件优先检测路径
path_sep = path.split(".")
if len(path_sep) >= 2 and path_sep[0] == "plugin":
plugin_path = "/www/server/panel/plugin/{}".format(path_sep[1])
if not os.path.isdir(plugin_path):
return None
module = import_module(path)
cls = getattr(module, cls_name, None)
if issubclass(cls, BaseTask):
return cls
elif isinstance(cls, BaseTask):
return cls.__class__
else:
debug_log("Error: The loaded class is not a subclass of BaseTask")
return None
except ModuleNotFoundError as e:
# todo暂时忽略 ssl_push
if 'mod.base.push_mod.ssl_push' in str(e):
return None
else:
debug_log(traceback.format_exc())
debug_log("ModuleNotFoundError: {}".format(str(e)))
return None
except Exception:
print(traceback.format_exc())
print(sys.path)
debug_log(traceback.format_exc())
return None

193
mod/base/push_mod/util.py Normal file
View File

@@ -0,0 +1,193 @@
import sys
from typing import Optional, Callable
if "/www/server/panel/class" not in sys.path:
sys.path.insert(0, "/www/server/panel/class")
import public
from db import Sql
import os
from sslModel import certModel
def write_file(filename: str, s_body: str, mode='w+') -> bool:
"""
写入文件内容
@filename 文件名
@s_body 欲写入的内容
return bool 若文件不存在则尝试自动创建
"""
try:
fp = open(filename, mode=mode)
fp.write(s_body)
fp.close()
return True
except:
try:
fp = open(filename, mode=mode, encoding="utf-8")
fp.write(s_body)
fp.close()
return True
except:
return False
def read_file(filename, mode='r') -> Optional[str]:
"""
读取文件内容
@filename 文件名
return string(bin) 若文件不存在则返回None
"""
import os
if not os.path.exists(filename):
return None
fp = None
try:
fp = open(filename, mode=mode)
f_body = fp.read()
except:
return None
finally:
if fp and not fp.closed:
fp.close()
return f_body
ExecShell: Callable = public.ExecShell
write_log: Callable = public.WriteLog
Sqlite: Callable = Sql
GET_CLASS: Callable = public.dict_obj
debug_log: Callable = public.print_log
get_config_value: Callable = public.GetConfigValue
get_server_ip: Callable = public.get_server_ip
get_network_ip: Callable = public.get_network_ip
format_date: Callable = public.format_date
public_get_cache_func: Callable = public.get_cache_func
public_set_cache_func: Callable = public.set_cache_func
public_get_user_info: Callable = public.get_user_info
public_http_post = public.httpPost
panel_version = public.version
try:
get_cert_list = certModel.main().get_cert_list
to_dict_obj = public.to_dict_obj
except:
public.print_log(public.get_error_info())
def get_client_ip() -> str:
return public.GetClientIp()
class _DB:
def __call__(self, table: str):
import db
with db.Sql() as t:
t.table(table)
return t
DB = _DB()
def check_site_status(web):
panelPath = '/www/server/panel/'
os.chdir(panelPath)
sys.path.insert(0, panelPath)
if web['project_type'] == "Java":
from mod.project.java.projectMod import main as java
if not java().get_project_stat(web)['pid']:
return None
if web['project_type'] == "Node":
from projectModelV2.nodejsModel import main as nodejs
if not nodejs().get_project_run_state(project_name=web['name']):
return None
if web['project_type'] == "Go":
from projectModel.goModel import main as go # NOQA
if not go().get_project_run_state(project_name=web['name']):
return None
if web['project_type'] == "Python":
from projectModelV2.pythonModel import main as python
if not python().get_project_run_state(project_name=web['name']):
return None
if web['project_type'] == "Other":
from projectModel.otherModel import main as other # NOQA
if not other().get_project_run_state(project_name=web['name']):
return None
return True
def get_db_by_file(file: str):
import db
if not os.path.exists(file):
return None
db_obj = db.Sql()
db_obj._Sql__DB_FILE = file
return db_obj
def generate_fields(template: dict, add_type: str) -> dict:
""""动态表单生成附加选项 hook"""
if add_type not in [
"restart", "module",
]:
return template
if add_type == "restart":
template = generate_restart_fields(template)
elif add_type == "module":
template = generate_module_fields(template)
return template
def generate_restart_fields(template: dict) -> dict:
"""动态表单生成重启服务 hook 选项"""
from script.restart_services import SERVICES_MAP, ServicesHelper
f = {
"attr": "after_hook",
"name": "After the alarm excutes",
"suffix": "select after alarm action (Optional)",
"type": "multiple-select",
"items": [
{
"title": f"Restart {x}",
"type": "restart",
"value": x
} for x in SERVICES_MAP.keys() if ServicesHelper(x).is_install
],
"default": []
}
if "field" in template:
template["field"].append(f)
else:
template["field"] = [f]
if "sorted" in template and isinstance(template["sorted"], list):
if ["after_hook"] not in template["sorted"]:
template["sorted"].append(["after_hook"])
elif "sorted" not in template:
template["sorted"] = [["after_hook"]]
else:
template["sorted"] = [template["sorted"], ["after_hook"]]
return template
def generate_module_fields(template: dict) -> dict:
"""动态表单生成模块调用 hook 选项"""
# todo
return template