├── MANIFEST.in ├── alert_test.png ├── feishu_app.png ├── alert_message.png ├── alert_ack_manager.png ├── zabbix_feishu_alert ├── __init__.py ├── message.py └── message_base.py ├── setup.py ├── .gitignore └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md *.png 2 | recursive-include tests *.py -------------------------------------------------------------------------------- /alert_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinhuanyi/zabbix-feishu-alert/HEAD/alert_test.png -------------------------------------------------------------------------------- /feishu_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinhuanyi/zabbix-feishu-alert/HEAD/feishu_app.png -------------------------------------------------------------------------------- /alert_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinhuanyi/zabbix-feishu-alert/HEAD/alert_message.png -------------------------------------------------------------------------------- /alert_ack_manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinhuanyi/zabbix-feishu-alert/HEAD/alert_ack_manager.png -------------------------------------------------------------------------------- /zabbix_feishu_alert/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | @Author: Robby 4 | @Module name: __init__.py.py 5 | @Create date: 2020-06-06 6 | @Function: 7 | """ 8 | 9 | from .message import * 10 | -------------------------------------------------------------------------------- /zabbix_feishu_alert/message.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | @Author: Robby 4 | @Module name: message.py 5 | @Create date: 2020-06-06 6 | @Function: 7 | """ 8 | 9 | import json 10 | 11 | import requests 12 | 13 | from .message_base import FeishuBase 14 | 15 | 16 | 17 | class FeishuMessage(FeishuBase): 18 | 19 | def _get_tenant_access_token(self, app_id, app_secret): 20 | tokenurl = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/" 21 | headers = {"Content-Type": "application/json"} 22 | data = {"app_id": app_id, 23 | "app_secret": app_secret} 24 | request = requests.post(url=tokenurl, headers=headers, json=data) 25 | response = json.loads(request.content)['tenant_access_token'] 26 | 27 | return response 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | @Author: Robby 4 | @Module name: setup.py 5 | @Create date: 2020-06-06 6 | @Function: 7 | """ 8 | 9 | import os 10 | from setuptools import setup 11 | 12 | def package_data(pkg, roots=tuple()): 13 | data = [] 14 | for root in roots: 15 | for dirname, _, files in os.walk(os.path.join(pkg, root)): 16 | for fname in files: 17 | print(os.path.relpath(os.path.join(dirname, fname), pkg)) 18 | data.append(os.path.relpath(os.path.join(dirname, fname), pkg)) 19 | 20 | return {pkg: data} 21 | 22 | with open("README.md", "r") as f: 23 | long_description = f.read() 24 | 25 | setup( 26 | name = 'zabbix-feishu-alert', 27 | author = 'Robby', 28 | author_email = 'yinhuanyicn@gmail.com', 29 | url = 'https://github.com/yinhuanyi/zabbix-feishu-alert', 30 | license = "MIT", 31 | version = '1.0.8', 32 | description = 'zabbix send alert message and graph to feishu robot', 33 | long_description = long_description, 34 | long_description_content_type = "text/markdown", 35 | packages = [ 36 | 'zabbix_feishu_alert', 37 | ], 38 | install_requires = [ 39 | 'requests', 40 | ], 41 | dependency_links = [], 42 | package_data = package_data("zabbix_feishu_alert",), 43 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | .dmypy.json 112 | dmypy.json 113 | 114 | # Pyre type checker 115 | .pyre/ 116 | 117 | # Delete idea 118 | .idea/ 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### (一)zabbix-feishu-alert 模块使用方法 2 | 3 | > **`(一):安装`** 4 | 5 | - 从 PYPI 安装 6 | 7 | ``` 8 | pip install -U zabbix-feishu-alert 9 | ``` 10 | 11 | - 从 Github 安装 12 | 13 | ``` 14 | pip install git+https://github.com/yinhuanyi/zabbix-feishu-alert.git 15 | ``` 16 | 17 | > **`(二):使用方法`** 18 | 19 | ``` 20 | from zabbix_feishu_alert import FeishuMessage 21 | 22 | # 第一个参数:100.99.1.3为你的zabbix serverIP地址 23 | # 第二个参数:Admin为你的zabbix web登录用户名 24 | # 第三个参数:zabbix为你的zabbix web登录密码 25 | # 第四个参数:13970236751为被@人的手机号码 26 | # 第五个参数:36836为监控item的item id 27 | # 第六个参数:zabbix graph存储路径 28 | # 第七个参数:飞书机器人的app_id 29 | # 第八个参数:飞书机器人的app_secret 30 | feishu = FeishuMessage('100.99.1.3', 31 | 'Admin', 32 | 'zabbix', 33 | '13970236751', 34 | 36836, 35 | './', 36 | 'cli_9e44d8e26dbb500d', 37 | '8X4jX9MLwg6AXIEVJh0lC8oeHNDBfbnd') 38 | 39 | # 第一个和第二个参数为:发送告警信息的时候,需要获取到zabbix中的title信息和message信息 40 | # 第三个参数:38524是此次告警的event_id 41 | # 第四个参数:http://100.112.2.11:8000/monitor/problem_ack/是[立即处理]按钮发送ACK消息webhook的地址 42 | feishu.send_alarm_message("Zabbix Alert Title", 43 | "Zabbix Alert Content", 44 | 38524, 45 | 'http://100.112.2.11:8000/monitor/problem_ack/') 46 | 47 | # 发送确认告警消息 48 | feishu.send_ack_message("Zabbix Ack Title", 49 | "Zabbix Content Title") 50 | 51 | # 发送恢复告警消息 52 | feishu.send_recovery_message("Zabbix Recovery Title", 53 | "Zabbix Content Title") 54 | ``` 55 | 56 | > **`(三):告警效果`** 57 | 58 | - 测试效果 59 | 60 | ![alert_test](./alert_test.png) 61 | 62 | - 真实接入zabbix之后的效果 63 | 64 | ![alert_message](./alert_message.png) 65 | 66 | 67 | > **`(四):点击[立即处理]按钮`** 68 | 69 | - 当值班人被@后,需要点击立即处理,立即处理会跳转到企业内部的运维平台,记录告警人的基本信息,例如:姓名,处理告警的时间等 70 | 71 | ![alert_ack_manager](./alert_ack_manager.png) 72 | 73 | 74 | ### (二)飞书机器人的创建 75 | 76 | > **`(一):登录飞书开放平台`** 77 | 78 | - 登录飞书开放 79 | 80 | [飞书开放平台](https://open.feishu.cn/) 81 | 82 | - 在我的应用中,点击创建企业自建应用 83 | 84 | - 在应用凭证栏中,可以看到APP ID和App Secret 85 | 86 | ![feishu_app](./feishu_app.png) 87 | 88 | # 欢迎提交PR -------------------------------------------------------------------------------- /zabbix_feishu_alert/message_base.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | @Author: Robby 4 | @Module name: message_base.py 5 | @Create date: 2020-06-06 6 | @Function: 7 | """ 8 | 9 | import os 10 | import json 11 | from datetime import datetime 12 | 13 | import requests 14 | from requests.cookies import RequestsCookieJar 15 | 16 | 17 | class FeishuBase: 18 | 19 | def __init__(self, zabbix_host, zabbix_user, zabbix_passwd, user_mobile, item_id, data_dir, app_id, app_secret): 20 | """ 21 | 22 | :param zabbix_host: zabbix ip address 23 | :param zabbix_user: zabbix admin username 24 | :param zabbix_passwd: zabbix admin passwd 25 | :param user_mobile: people mobile 26 | :param item_id: zabbix item id 27 | :param data_dir: zabbix graph storage directory 28 | """ 29 | 30 | self.tenant_access_token = self._get_tenant_access_token(app_id, app_secret) 31 | self.chat_id = self._get_chat_id(self.tenant_access_token) 32 | self.user_id = self._get_user_id(self.tenant_access_token, user_mobile) 33 | self.zabbix_graph = self._get_zabbix_graph(item_id, zabbix_host, zabbix_user, zabbix_passwd, data_dir) 34 | self.image_key = self._upload_zabbix_graph(self.tenant_access_token, self.zabbix_graph) 35 | 36 | def _get_tenant_access_token(self, *args, **kwargs): 37 | raise Exception("Please Implement This Method") 38 | 39 | def _get_user_id(self, tenant_access_token, user_mobile): 40 | """ 41 | 42 | :param tenant_access_token: feishu tenant_access_token 43 | :param user_mobile: people mobile 44 | :return: user id 45 | """ 46 | mobiles = user_mobile 47 | userurl = "https://open.feishu.cn/open-apis/user/v1/batch_get_id?mobiles=%s" % mobiles 48 | headers = {"Authorization": "Bearer %s" % tenant_access_token} 49 | request = requests.get(url=userurl, headers=headers) 50 | response = json.loads(request.content)['data']['mobile_users'][mobiles][0]['user_id'] 51 | return response 52 | 53 | def _get_chat_id(self, tenant_access_token): 54 | """ 55 | 56 | :param tenant_access_token: feishu tenant_access_token 57 | :return: chat id 58 | """ 59 | chaturl = "https://open.feishu.cn/open-apis/chat/v4/list?page_size=20" 60 | headers = {"Authorization": "Bearer %s" % tenant_access_token, "Content-Type": "application/json"} 61 | request = requests.get(url=chaturl, headers=headers) 62 | response = json.loads(request.content)['data']['groups'][0]['chat_id'] 63 | return response 64 | 65 | def _get_zabbix_graph(self, item_id, zabbix_host, zabbix_user, zabbix_passwd, data_dir): 66 | """ 67 | 68 | :param item_id: zabbix item id 69 | :param zabbix_host: zabbix ip addr 70 | :param zabbix_user: zabbix admin username 71 | :param zabbix_passwd: zabbix admin passwd 72 | :param data_dir: zabbix graph storage directory 73 | :return: local absolute zabbix graph path name 74 | """ 75 | # 创建session会话 76 | session = requests.Session() 77 | 78 | # 定义session头部 79 | loginheaders = { 80 | "Host": zabbix_host, 81 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 82 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36', 83 | 'Referer': 'http://{}/zabbix/index.php'.format(zabbix_host) 84 | } 85 | 86 | # 定义payload 87 | payload = { 88 | "name": zabbix_user, 89 | "password": zabbix_passwd, 90 | "autologin": 1, 91 | "enter": "Sign in", 92 | } 93 | 94 | try: 95 | # session登录 96 | login_ret = session.post(url='http://{}/zabbix/index.php'.format(zabbix_host), 97 | headers=loginheaders, 98 | data=payload) 99 | # 获取cookie 100 | cookies = login_ret.cookies 101 | 102 | # 初始化jar,写入cookie 103 | jar = RequestsCookieJar() 104 | for item in cookies.iteritems(): 105 | jar.set(item[0], item[1], domain='{}'.format(zabbix_host), path='/zabbix') 106 | 107 | # 访问图标 108 | graph_response = requests.get('http://{}/zabbix/chart.php?period=7200&width=600&time=600&itemids={}'.format(zabbix_host, item_id),cookies=jar) 109 | 110 | # 拼接图片路径 111 | local_time_str = datetime.now().strftime('%Y-%m-%d_%H:%M:%S') 112 | graph_name = 'zabbix_' + local_time_str + '.png' 113 | 114 | graph_path = os.path.join(data_dir, graph_name) 115 | 116 | # 使用绝对路径保存图片,二进制写入 117 | with open(graph_path, 'wb', ) as f: 118 | f.write(graph_response.content) 119 | 120 | # 返回图片名称 121 | return graph_path 122 | 123 | except Exception: 124 | raise Exception("get zabbix graph failed") 125 | 126 | def _upload_zabbix_graph(self, tenant_access_token, graph_path): 127 | """ 128 | 129 | :param tenant_access_token: feishu tenant_access_token 130 | :param graph_path: local absolute zabbix graph path name 131 | :return: 132 | """ 133 | with open(graph_path, 'rb') as f: 134 | image = f.read() 135 | 136 | img_url = 'https://open.feishu.cn/open-apis/image/v4/put/' 137 | headers = {'Authorization': "Bearer %s" % tenant_access_token} 138 | files = {"image": image} 139 | data = {"image_type": "message"} 140 | resp = requests.post( 141 | url=img_url, 142 | headers=headers, 143 | files=files, 144 | data=data 145 | ) 146 | resp.raise_for_status() 147 | content = resp.json() 148 | 149 | # 获取上传的image_key 150 | return content['data']['image_key'] 151 | 152 | # 发送告警消息 153 | def send_alarm_message(self, title, content, event_id, zabbix_ack_addr): 154 | """ 155 | 156 | :param user_id: user id 157 | :param chat_id: chat id 158 | :param tenant_access_token: feishu tenant_access_token 159 | :param image_key: feishu image key 160 | :param title: zabbix alart title 161 | :param content: zabbix alart content 162 | :param event_id: zabbix event id 163 | :param zabbix_ack_addr: your website for zabbix alert ack addr 164 | :return: None 165 | """ 166 | 167 | send_url = "https://open.feishu.cn/open-apis/message/v4/send/" 168 | headers = {"Authorization": "Bearer %s" % self.tenant_access_token, "Content-Type": "application/json"} 169 | data = { 170 | "chat_id": self.chat_id, 171 | "msg_type": "post", 172 | "content": { 173 | "post": { 174 | "zh_cn": { 175 | "title": title, 176 | "content": [ 177 | [ 178 | { 179 | "tag": "text", 180 | "un_escape": True, 181 | "text": content 182 | }, 183 | { 184 | "tag": "at", 185 | "user_id": self.user_id 186 | 187 | }, 188 | { 189 | "tag": "a", 190 | "text": "\n立即处理", 191 | # http://{}:8000/monitor/problem_ack/ 192 | "href": "{}?event_id={}".format(zabbix_ack_addr, event_id) 193 | }, 194 | ], 195 | [ 196 | { 197 | "tag": "img", 198 | "image_key": self.image_key, 199 | "width": 1000, 200 | "height": 600 201 | } 202 | ] 203 | ] 204 | } 205 | } 206 | } 207 | } 208 | 209 | requests.post(url=send_url, headers=headers, json=data) 210 | 211 | # 发送恢复消息 212 | def send_recovery_message(self, title, content): 213 | 214 | """ 215 | :param title: zabbix alert title 216 | :param content: zabbix alert content 217 | :return: None 218 | """ 219 | sendurl = "https://open.feishu.cn/open-apis/message/v4/send/" 220 | headers = {"Authorization": "Bearer %s" % self.tenant_access_token, "Content-Type": "application/json"} 221 | data = { 222 | "chat_id": self.chat_id, 223 | "msg_type": "post", 224 | "content": { 225 | "post": { 226 | "zh_cn": { 227 | "title": title, 228 | "content": [ 229 | [ 230 | { 231 | "tag": "text", 232 | "un_escape": True, 233 | "text": content 234 | }, 235 | { 236 | "tag": "at", 237 | "user_id": self.user_id 238 | 239 | }, 240 | ], 241 | [ 242 | { 243 | "tag": "img", 244 | "image_key": self.image_key, 245 | "width": 1000, 246 | "height": 600 247 | } 248 | ] 249 | ] 250 | } 251 | } 252 | } 253 | } 254 | requests.post(url=sendurl, headers=headers, json=data) 255 | 256 | # 发送确认消息 257 | def send_ack_message(self, title, content): 258 | """ 259 | 260 | :param title: zabbix alert title 261 | :param content: zabbix alert content 262 | :return: None 263 | """ 264 | sendurl = "https://open.feishu.cn/open-apis/message/v4/send/" 265 | headers = {"Authorization": "Bearer %s" % self.tenant_access_token, "Content-Type": "application/json"} 266 | data = { 267 | "chat_id": self.chat_id, 268 | "msg_type": "post", 269 | "content": { 270 | "post": { 271 | "zh_cn": { 272 | "title": title, 273 | "content": [ 274 | [ 275 | { 276 | "tag": "text", 277 | "un_escape": True, 278 | "text": content 279 | }, 280 | { 281 | "tag": "at", 282 | "user_id": self.user_id 283 | }, 284 | ], 285 | [ 286 | { 287 | "tag": "img", 288 | "image_key": self.image_key, 289 | "width": 1000, 290 | "height": 600 291 | } 292 | ] 293 | ] 294 | } 295 | } 296 | } 297 | } 298 | requests.post(url=sendurl, headers=headers, json=data) 299 | --------------------------------------------------------------------------------