629 lines
17 KiB
Python
629 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
@File : notify_service.py
|
|
@Date : 2022-10-30
|
|
@Author : Peng Shiyu
|
|
"""
|
|
from __future__ import print_function, unicode_literals, absolute_import, division
|
|
|
|
import json
|
|
import traceback
|
|
from datetime import datetime, timedelta
|
|
|
|
import requests
|
|
from jinja2 import Template
|
|
from playhouse.shortcuts import model_to_dict
|
|
|
|
from domain_admin.enums.config_key_enum import ConfigKeyEnum
|
|
from domain_admin.enums.event_enum import EventEnum
|
|
from domain_admin.enums.notify_type_enum import NotifyTypeEnum
|
|
from domain_admin.log import logger
|
|
from domain_admin.model.certificate_model import CertificateModel
|
|
from domain_admin.model.domain_info_model import DomainInfoModel
|
|
from domain_admin.model.domain_model import DomainModel
|
|
from domain_admin.model.group_user_model import GroupUserModel
|
|
from domain_admin.model.notify_model import NotifyModel
|
|
from domain_admin.service import domain_service, render_service, system_service, async_task_service, group_service
|
|
from domain_admin.utils import email_util, json_util
|
|
from domain_admin.utils.flask_ext.app_exception import AppException
|
|
from domain_admin.utils.open_api import feishu_api, work_weixin_api, ding_talk_api, telegram_api
|
|
|
|
# 通知参数配置
|
|
NOTIFY_CONFIGS = [
|
|
{
|
|
'event_id': EventEnum.SSL_CERT_EXPIRE,
|
|
'email_template': 'cert-email.html',
|
|
'email_subject': '[Domain Admin]证书过期提醒',
|
|
},
|
|
{
|
|
'event_id': EventEnum.DOMAIN_EXPIRE,
|
|
'email_template': 'domain-email.html',
|
|
'email_subject': '[Domain Admin]域名过期提醒',
|
|
},
|
|
{
|
|
'event_id': EventEnum.MONITOR_EXCEPTION,
|
|
'email_template': 'monitor-email.html',
|
|
'email_subject': '[Domain Admin]监控异常提醒',
|
|
},
|
|
{
|
|
'event_id': EventEnum.MONITOR_EXCEPTION_RESTORE,
|
|
'email_template': 'monitor-restore-email.html',
|
|
'email_subject': '[Domain Admin]监控异常恢复提醒',
|
|
},
|
|
{
|
|
'event_id': EventEnum.SSL_CERT_FILE_EXPIRE,
|
|
'email_template': 'cert-email.html',
|
|
'email_subject': '[Domain Admin]托管证书到期提醒',
|
|
}
|
|
]
|
|
|
|
|
|
def get_notify_config(event_id):
|
|
"""
|
|
获取通知配置
|
|
:param event_id: int
|
|
:return:
|
|
"""
|
|
for config in NOTIFY_CONFIGS:
|
|
if config['event_id'] == event_id:
|
|
return config
|
|
|
|
|
|
def get_notify_row_value(user_id, type_id):
|
|
"""
|
|
获取通知配置
|
|
:param user_id:
|
|
:param type_id:
|
|
:return:
|
|
"""
|
|
notify_row = NotifyModel.select().where(
|
|
NotifyModel.user_id == user_id,
|
|
NotifyModel.type_id == type_id
|
|
).get_or_none()
|
|
|
|
if not notify_row:
|
|
return None
|
|
|
|
if not notify_row.value:
|
|
return None
|
|
|
|
return notify_row.value
|
|
|
|
|
|
def get_notify_email_list_of_user(user_id):
|
|
"""
|
|
获取通知配置 - 邮箱列表
|
|
:param user_id:
|
|
:return: Optional[List[str]]
|
|
"""
|
|
notify_row_value = get_notify_row_value(user_id, NotifyTypeEnum.Email)
|
|
|
|
if not notify_row_value:
|
|
return None
|
|
|
|
email_list = notify_row_value.get('email_list')
|
|
|
|
if not email_list:
|
|
return None
|
|
|
|
return email_list
|
|
|
|
|
|
def get_notify_webhook_row_of_user(user_id):
|
|
"""
|
|
获取通知配置 - webhook
|
|
:param user_id:
|
|
:return:
|
|
"""
|
|
return get_notify_row_value(user_id, NotifyTypeEnum.WebHook)
|
|
|
|
|
|
def notify_webhook_of_user(user_id):
|
|
"""
|
|
通过 webhook 方式通知用户
|
|
:param user_id:
|
|
:return:
|
|
"""
|
|
notify_webhook_row = get_notify_webhook_row_of_user(user_id)
|
|
|
|
if not notify_webhook_row:
|
|
raise AppException('webhook未设置')
|
|
|
|
method = notify_webhook_row.get('method')
|
|
url = notify_webhook_row.get('url')
|
|
headers = notify_webhook_row.get('headers')
|
|
body = notify_webhook_row.get('body')
|
|
|
|
if not url:
|
|
raise AppException('url未设置')
|
|
|
|
# 支持模板变量
|
|
template = Template(body)
|
|
body_render = template.render(get_template_data(user_id))
|
|
|
|
res = requests.request(method=method, url=url, headers=headers, data=body_render.encode('utf-8'))
|
|
res.encoding = res.apparent_encoding
|
|
|
|
return res.text
|
|
|
|
|
|
def get_template_data(user_id):
|
|
# 两种参数形式
|
|
domain_list = domain_service.get_domain_info_list(user_id)
|
|
return {
|
|
'domain_list': domain_list
|
|
}
|
|
|
|
|
|
def notify_all_event():
|
|
"""
|
|
触发所有通知事件
|
|
:return: int 成功数量
|
|
"""
|
|
# fix: TypeError: inner() takes 2 positional arguments but 3 were given
|
|
rows = NotifyModel.select().where(
|
|
NotifyModel.status == True,
|
|
NotifyModel.event_id.in_([
|
|
EventEnum.SSL_CERT_EXPIRE,
|
|
EventEnum.DOMAIN_EXPIRE,
|
|
EventEnum.SSL_CERT_FILE_EXPIRE
|
|
])
|
|
)
|
|
|
|
success = 0
|
|
for row in rows:
|
|
try:
|
|
notify_user_about_some_event(row)
|
|
except:
|
|
logger.error(traceback.format_exc())
|
|
|
|
success = success + 1
|
|
|
|
return success
|
|
|
|
|
|
def notify_user_about_monitor_exception(monitor_row, error):
|
|
rows = NotifyModel.select().where(
|
|
NotifyModel.status == True,
|
|
NotifyModel.event_id == EventEnum.MONITOR_EXCEPTION
|
|
)
|
|
|
|
for row in rows:
|
|
try:
|
|
notify_user(notify_row=row, rows=rows, data={'monitor_row': monitor_row, 'error': str(error)})
|
|
except:
|
|
logger.error(traceback.format_exc())
|
|
|
|
|
|
def notify_user_about_monitor_exception_restore(monitor_row):
|
|
"""
|
|
监控异常恢复通知
|
|
:param monitor_row:
|
|
:param error:
|
|
:return:
|
|
"""
|
|
rows = NotifyModel.select().where(
|
|
NotifyModel.status == True,
|
|
NotifyModel.event_id == EventEnum.MONITOR_EXCEPTION_RESTORE
|
|
)
|
|
|
|
for row in rows:
|
|
try:
|
|
notify_user(notify_row=row, rows=rows, data={'monitor_row': monitor_row})
|
|
except:
|
|
logger.error(traceback.format_exc())
|
|
|
|
|
|
# @async_task_service.sync_task_decorator("触发通知用户")
|
|
def notify_user_about_some_event(notify_row):
|
|
"""
|
|
由于某个事件触发,通知用户
|
|
:param notify_row: NotifyModel
|
|
:return:
|
|
"""
|
|
if notify_row.event_id == EventEnum.SSL_CERT_EXPIRE:
|
|
# ssl证书
|
|
return notify_user_about_cert_expired(notify_row)
|
|
elif notify_row.event_id == EventEnum.DOMAIN_EXPIRE:
|
|
# 域名过期
|
|
return notify_user_about_domain_expired(notify_row)
|
|
elif notify_row.event_id == EventEnum.SSL_CERT_FILE_EXPIRE:
|
|
# 托管证书到期
|
|
return notify_user_about_cert_file_expired(notify_row)
|
|
else:
|
|
raise AppException("notify_row event_id not support: {}".format(notify_row.event_id))
|
|
|
|
|
|
def notify_user_about_cert_expired(notify_row):
|
|
"""
|
|
证书过期事件触发
|
|
:param notify_row: NotifyModel
|
|
:return:
|
|
"""
|
|
now = datetime.now()
|
|
|
|
notify_expire_time = now + timedelta(days=notify_row.expire_days)
|
|
|
|
# 所在分组
|
|
group_user_rows = GroupUserModel.select().where(
|
|
GroupUserModel.user_id == notify_row.user_id
|
|
)
|
|
|
|
group_user_list = list(group_user_rows)
|
|
user_group_ids = [row.group_id for row in group_user_list]
|
|
|
|
# 注意null的情况
|
|
query = DomainModel.select().where(
|
|
DomainModel.is_monitor == True
|
|
)
|
|
|
|
# 分组
|
|
if notify_row.groups:
|
|
query = query.where(
|
|
DomainModel.group_id.in_(notify_row.groups)
|
|
)
|
|
elif user_group_ids:
|
|
query = query.where(
|
|
(DomainModel.user_id == notify_row.user_id)
|
|
| (DomainModel.group_id.in_(user_group_ids))
|
|
)
|
|
else:
|
|
query = query.where(
|
|
DomainModel.user_id == notify_row.user_id,
|
|
)
|
|
|
|
rows = query.where(
|
|
DomainModel.expire_time.is_null(False),
|
|
DomainModel.expire_time <= notify_expire_time
|
|
).order_by(
|
|
DomainModel.expire_time.asc(),
|
|
DomainModel.id.desc()
|
|
)
|
|
|
|
lst = [model_to_dict(
|
|
model=row,
|
|
extra_attrs=[
|
|
'start_date',
|
|
'expire_date',
|
|
'real_time_expire_days',
|
|
'update_time_label',
|
|
]
|
|
) for row in rows]
|
|
|
|
for row in lst:
|
|
row['expire_days'] = row['real_time_expire_days']
|
|
row['comment'] = row['alias']
|
|
row['is_auto_update'] = row['auto_update']
|
|
row['is_expire_monitor'] = row['is_monitor']
|
|
|
|
group_service.load_group_name(lst)
|
|
|
|
if len(lst) > 0:
|
|
return notify_user(notify_row, lst)
|
|
|
|
|
|
def notify_user_about_cert_file_expired(notify_row):
|
|
"""
|
|
托管证书到期
|
|
:param notify_row:
|
|
:return:
|
|
"""
|
|
now = datetime.now()
|
|
|
|
notify_expire_time = now + timedelta(days=notify_row.expire_days)
|
|
|
|
# 注意null的情况
|
|
query = CertificateModel.select()
|
|
|
|
rows = query.where(
|
|
CertificateModel.expire_time.is_null(False),
|
|
CertificateModel.expire_time <= notify_expire_time
|
|
).order_by(
|
|
CertificateModel.expire_time.asc(),
|
|
CertificateModel.id.desc()
|
|
)
|
|
|
|
lst = [row.to_dict() for row in rows]
|
|
|
|
for row in lst:
|
|
row['expire_days'] = row['real_time_expire_days']
|
|
|
|
if len(lst) > 0:
|
|
return notify_user(notify_row, lst)
|
|
|
|
|
|
def notify_user_about_domain_expired(notify_row):
|
|
"""
|
|
域名过期事件触发
|
|
:param notify_row: NotifyModel
|
|
:return:
|
|
"""
|
|
now = datetime.now()
|
|
|
|
notify_expire_time = now + timedelta(days=notify_row.expire_days)
|
|
|
|
# 所在分组
|
|
group_user_rows = GroupUserModel.select().where(
|
|
GroupUserModel.user_id == notify_row.user_id
|
|
)
|
|
|
|
group_user_list = list(group_user_rows)
|
|
user_group_ids = [row.group_id for row in group_user_list]
|
|
|
|
# 注意null的情况
|
|
query = DomainInfoModel.select().where(
|
|
DomainInfoModel.is_expire_monitor == True
|
|
)
|
|
|
|
if notify_row.groups:
|
|
query = query.where(
|
|
DomainInfoModel.group_id.in_(notify_row.groups)
|
|
)
|
|
elif user_group_ids:
|
|
query = query.where(
|
|
(DomainInfoModel.user_id == notify_row.user_id)
|
|
| (DomainInfoModel.group_id.in_(user_group_ids))
|
|
)
|
|
else:
|
|
query = query.where(
|
|
DomainInfoModel.user_id == notify_row.user_id,
|
|
)
|
|
|
|
rows = query.where(
|
|
(DomainInfoModel.domain_expire_time <= notify_expire_time)
|
|
| (DomainInfoModel.domain_expire_time.is_null(True))
|
|
).order_by(
|
|
DomainInfoModel.domain_expire_time.asc(),
|
|
DomainInfoModel.id.desc()
|
|
)
|
|
|
|
lst = [model_to_dict(
|
|
model=row,
|
|
extra_attrs=[
|
|
'tags',
|
|
'start_date',
|
|
'expire_date',
|
|
'expire_days',
|
|
'tags_str',
|
|
'update_time_label',
|
|
]
|
|
) for row in rows]
|
|
|
|
for row in lst:
|
|
row['start_time'] = row['domain_start_time']
|
|
row['expire_time'] = row['domain_expire_time']
|
|
# row['start_date'] = row['domain_start_date']
|
|
# row['expire_date'] = row['domain_expire_date']
|
|
# row['expire_days'] = row['real_domain_expire_days']
|
|
group_service.load_group_name(lst)
|
|
|
|
if len(lst) > 0:
|
|
return notify_user(notify_row, lst)
|
|
|
|
|
|
def notify_user(notify_row, rows, data=None):
|
|
"""
|
|
通知用户
|
|
:param notify_row: NotifyModel
|
|
:param rows: List
|
|
:return:
|
|
"""
|
|
logger.debug(json_util.json_dump(rows))
|
|
|
|
data = data if data else {}
|
|
|
|
# 通知用户
|
|
if notify_row.type_id == NotifyTypeEnum.Email:
|
|
notify_config = get_notify_config(notify_row.event_id)
|
|
|
|
if not notify_config:
|
|
raise AppException('邮件通知模板未配置')
|
|
|
|
return notify_user_by_email(
|
|
template=notify_config['email_template'],
|
|
subject=notify_config['email_subject'],
|
|
data={**data, 'list': rows},
|
|
email_list=notify_row.email_list
|
|
)
|
|
elif notify_row.type_id == NotifyTypeEnum.WebHook:
|
|
return notify_user_by_webhook(
|
|
notify_row=notify_row,
|
|
data={
|
|
**data,
|
|
'list': rows,
|
|
'domain_list': rows # 兼容旧版本
|
|
})
|
|
elif notify_row.type_id == NotifyTypeEnum.WORK_WEIXIN:
|
|
return notify_user_by_work_weixin(notify_row=notify_row, data={**data, 'list': rows})
|
|
|
|
elif notify_row.type_id == NotifyTypeEnum.DING_TALK:
|
|
return notify_user_by_ding_talk(notify_row=notify_row, data={**data, 'list': rows})
|
|
|
|
elif notify_row.type_id == NotifyTypeEnum.FEISHU:
|
|
return notify_user_by_feishu(notify_row=notify_row, data={**data, 'list': rows})
|
|
|
|
elif notify_row.type_id == NotifyTypeEnum.Telegram:
|
|
return notify_user_by_telegram(notify_row=notify_row, data={**data, 'list': rows})
|
|
|
|
else:
|
|
logger.warn("type not support")
|
|
|
|
|
|
@async_task_service.sync_task_decorator("触发Webhook通知")
|
|
def notify_user_by_webhook(
|
|
notify_row,
|
|
data):
|
|
"""
|
|
通过 webhook 方式通知用户
|
|
:param notify_row: NotifyModel
|
|
:param data: Dict
|
|
:return:
|
|
"""
|
|
logger.info("notify_row.id: %s", notify_row.id)
|
|
|
|
if not notify_row.webhook_url:
|
|
logger.warn("webhook url未设置")
|
|
return
|
|
|
|
# 支持模板变量
|
|
template = Template(notify_row.webhook_body)
|
|
body_render = template.render(data)
|
|
|
|
logger.info(body_render)
|
|
|
|
res = requests.request(
|
|
method=notify_row.webhook_method,
|
|
url=notify_row.webhook_url,
|
|
headers=notify_row.webhook_headers,
|
|
data=body_render.encode('utf-8'))
|
|
|
|
res.encoding = res.apparent_encoding
|
|
logger.info(res.text)
|
|
return res.text
|
|
|
|
|
|
@async_task_service.sync_task_decorator("触发邮件通知")
|
|
def notify_user_by_email(
|
|
template,
|
|
subject,
|
|
data,
|
|
email_list,
|
|
):
|
|
"""
|
|
通过邮件通知用户证书到期
|
|
:param template: str
|
|
:param subject: str
|
|
:param data: Dict
|
|
:param email_list: List[str]
|
|
:return:
|
|
"""
|
|
send_email_to_user(
|
|
template,
|
|
subject,
|
|
data,
|
|
email_list
|
|
)
|
|
|
|
|
|
def send_email_to_user(
|
|
template,
|
|
subject,
|
|
data,
|
|
email_list,
|
|
):
|
|
"""
|
|
通过邮件通知用户证书到期
|
|
:param template: str
|
|
:param subject: str
|
|
:param data: Dict
|
|
:param email_list: List[str]
|
|
:return:
|
|
"""
|
|
if not email_list or len(email_list) == 0:
|
|
logger.warn("email_list is empty")
|
|
return
|
|
|
|
content = render_service.render_template(
|
|
template=template,
|
|
data=data
|
|
)
|
|
|
|
config = system_service.get_system_config()
|
|
|
|
email_util.send_email(
|
|
mail_host=config[ConfigKeyEnum.MAIL_HOST],
|
|
mail_port=int(config[ConfigKeyEnum.MAIL_PORT]),
|
|
mail_alias=config[ConfigKeyEnum.MAIL_ALIAS],
|
|
subject=subject,
|
|
content=content,
|
|
to_addresses=email_list,
|
|
mail_username=config[ConfigKeyEnum.MAIL_USERNAME],
|
|
mail_password=config[ConfigKeyEnum.MAIL_PASSWORD],
|
|
content_type='html'
|
|
)
|
|
|
|
|
|
@async_task_service.sync_task_decorator("触发企业微信通知")
|
|
def notify_user_by_work_weixin(notify_row, data):
|
|
"""
|
|
发送企业微信消息
|
|
:param notify_row: NotifyModel
|
|
:return:
|
|
"""
|
|
token = work_weixin_api.get_access_token(notify_row.work_weixin_corpid, notify_row.work_weixin_corpsecret)
|
|
logger.info('work weixin token %s', token)
|
|
|
|
# 支持模板变量
|
|
template = Template(notify_row.work_weixin_body)
|
|
work_weixin_body = template.render(data)
|
|
|
|
res = work_weixin_api.send_message(token['access_token'], json.loads(work_weixin_body))
|
|
return res
|
|
|
|
|
|
@async_task_service.sync_task_decorator("触发钉钉通知")
|
|
def notify_user_by_ding_talk(notify_row, data):
|
|
"""
|
|
发送钉钉消息
|
|
:param notify_row: NotifyModel
|
|
:return:
|
|
"""
|
|
token = ding_talk_api.get_access_token(notify_row.ding_talk_appkey, notify_row.ding_talk_appsecret)
|
|
logger.info('ding talk token %s', token)
|
|
|
|
# 支持模板变量
|
|
template = Template(notify_row.ding_talk_body)
|
|
ding_talk_body = template.render(data)
|
|
|
|
res = ding_talk_api.send_message(token['access_token'], json.loads(ding_talk_body))
|
|
return res
|
|
|
|
|
|
@async_task_service.sync_task_decorator("触发飞书通知")
|
|
def notify_user_by_feishu(notify_row, data):
|
|
"""
|
|
发送飞书消息
|
|
:param notify_row: NotifyModel
|
|
:return:
|
|
"""
|
|
token = feishu_api.get_access_token(notify_row.feishu_app_id, notify_row.feishu_app_secret)
|
|
logger.info('feishu token %s', token)
|
|
|
|
# 支持模板变量
|
|
template = Template(notify_row.feishu_body)
|
|
feishu_body = template.render(data)
|
|
|
|
res = feishu_api.send_message(
|
|
access_token=token['tenant_access_token'],
|
|
body=json.loads(feishu_body),
|
|
params=notify_row.feishu_params
|
|
)
|
|
|
|
return res
|
|
|
|
|
|
@async_task_service.sync_task_decorator("触发电报通知")
|
|
def notify_user_by_telegram(notify_row: NotifyModel, data):
|
|
"""
|
|
触发电报通知
|
|
:param notify_row: NotifyModel
|
|
:return:
|
|
"""
|
|
|
|
# 支持模板变量
|
|
template = Template(notify_row.telegram_body)
|
|
telegram_body = template.render(data)
|
|
|
|
data = {
|
|
'token': notify_row.telegram_token,
|
|
'chat_id': notify_row.telegram_chat_id,
|
|
'text': telegram_body,
|
|
'proxies': notify_row.telegram_proxies,
|
|
}
|
|
|
|
logger.info(data)
|
|
|
|
res = telegram_api.send_message(**data)
|
|
|
|
return res
|