# coding: utf-8 # ------------------------------------------------------------------- # yakpanel # ------------------------------------------------------------------- # Copyright (c) 2015-2017 yakpanel(https://www.yakpanel.com) All rights reserved. # ------------------------------------------------------------------- # Author: baozi # ------------------------------------------------------------------- # 新告警的所有数据库操作 # ------------------------------ 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)