From 3ec90da1e0c5206a90788d55ccfc929c840d4c8c Mon Sep 17 00:00:00 2001 From: "s.mostryukov" Date: Thu, 13 Mar 2025 18:10:30 +0300 Subject: [PATCH 1/5] add bot --- docker/docker-compose.yaml | 2 +- main.py | 22 +++++++++++----- telegram/__init__.py | 2 ++ telegram/bot.py | 52 ++++++++++++++++++++++++++++++++++++++ telegram/message.py | 15 ++++++++--- zabbix/__init__.py | 4 ++- zabbix/zabbix_api.py | 38 ++++++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 telegram/bot.py diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index be78013..cab7fc2 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -9,7 +9,7 @@ services: command: [redis-server, --protected-mode yes, --port 6379, --requirepass, P@ssw0rd!] tg-bot: - image: git.sm8255082.ru/osnova/zbx-tg-bot:1.0.0 + image: git.sm8255082.ru/osnova/zbx-tg-bot:1.1.0 restart: always depends_on: - redis diff --git a/main.py b/main.py index 9cf1785..125ebe7 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,6 @@ import logging as log from zabbix import get_active_problems from config import conf, icon_dict -from time import sleep from redis_db import ( get_all_keys, get_value, @@ -9,10 +8,10 @@ from redis_db import ( del_value, ) import asyncio -from telegram import del_message, send_message +from telegram import del_message, send_message, start_bot -async def main_loop(): +async def dashboard(): active_alerts = get_active_problems() if active_alerts is None: return @@ -38,7 +37,7 @@ async def main_loop(): + f"{active_alerts[new_alert]['host']}\n" + f"{active_alerts[new_alert]['name']}" ) - msg_id = await send_message(message) + msg_id = await send_message(message=message, event_id=new_alert) if msg_id["status"] == 200: await set_value(key=new_alert, value=msg_id["msg_id"]) @@ -53,12 +52,21 @@ async def main_loop(): await del_value(closed_alert) +async def dashboard_loop(): + log.info("Dashboard loop started") + while True: + await dashboard() + await asyncio.sleep(conf.zabbix.upd_interval) + + +async def main(): + await asyncio.gather(dashboard_loop(), start_bot()) + + if __name__ == "__main__": log.info("Starting app") try: - while True: - asyncio.run(main_loop()) - sleep(conf.zabbix.upd_interval) + asyncio.run(main()) except KeyboardInterrupt: log.info("Manual app stopped") log.info("App stopped") diff --git a/telegram/__init__.py b/telegram/__init__.py index e82d7ff..9a89e27 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -2,8 +2,10 @@ from .message import ( send_message, del_message, ) +from .bot import start_bot __all__ = [ "send_message", "del_message", + "start_bot", ] diff --git a/telegram/bot.py b/telegram/bot.py new file mode 100644 index 0000000..67695b0 --- /dev/null +++ b/telegram/bot.py @@ -0,0 +1,52 @@ +from config import conf +from aiogram import Bot, Dispatcher, types +from aiogram.fsm.storage.memory import MemoryStorage +from aiogram import F +from zabbix import event_acknowledge, event_close +import logging as log + +tg_bot = Bot(token=conf.tgbot.token) +storage = MemoryStorage() +dp = Dispatcher(storage=storage) + + +@dp.callback_query(F.data.startswith("h")) +async def handle_mute_1h(callback_query: types.CallbackQuery): + + if event_acknowledge(int(callback_query.data[1:]), 1): + new_text = ( + callback_query.message.text + + "\n" + + callback_query.from_user.username + + " Замьютил на час" + ) + await callback_query.message.edit_text(new_text) + + +@dp.callback_query(F.data.startswith("d")) +async def handle_mute_1d(callback_query: types.CallbackQuery): + if event_acknowledge(int(callback_query.data[1:]), 24): + new_text = ( + callback_query.message.text + + "\n" + + callback_query.from_user.username + + " Замьютил на сутки" + ) + await callback_query.message.edit_text(new_text) + + +@dp.callback_query(F.data.startswith("c")) +async def handle_close(callback_query: types.CallbackQuery): + if event_close(int(callback_query.data[1:])): + new_text = ( + callback_query.message.text + + "\n" + + callback_query.from_user.username + + " Закрыл" + ) + await callback_query.message.edit_text(new_text) + + +async def start_bot(): + log.info("Telegram bot loop started") + await dp.start_polling(tg_bot) diff --git a/telegram/message.py b/telegram/message.py index a2908d9..2a3922f 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -2,16 +2,25 @@ import logging as log import aiohttp from config import conf +import json -async def send_message( - message: str, -) -> dict: +async def send_message(message: str, event_id: int) -> dict: url = f"https://api.telegram.org/bot{conf.tgbot.token}/sendMessage" + inline_buttons = [ + [ + {"text": "🛠 на 1 час", "callback_data": f"h{event_id}"}, + {"text": "🛠 на 1 день", "callback_data": f"d{event_id}"}, + {"text": "✅ Закрыть", "callback_data": f"c{event_id}"}, + ], + ] + reply_markup = {"inline_keyboard": inline_buttons} + params = { "chat_id": conf.tgbot.chat_id, "message_thread_id": conf.tgbot.tread_id, "text": message, + "reply_markup": json.dumps(reply_markup), } async with aiohttp.ClientSession() as session: diff --git a/zabbix/__init__.py b/zabbix/__init__.py index 7c9de56..7a4de3f 100644 --- a/zabbix/__init__.py +++ b/zabbix/__init__.py @@ -1,6 +1,8 @@ -from .zabbix_api import get_active_problems +from .zabbix_api import get_active_problems, event_acknowledge, event_close __all__ = [ "get_active_problems", + "event_acknowledge", + "event_close", ] diff --git a/zabbix/zabbix_api.py b/zabbix/zabbix_api.py index 0036abd..180c47b 100644 --- a/zabbix/zabbix_api.py +++ b/zabbix/zabbix_api.py @@ -4,6 +4,8 @@ from zabbix_utils import ZabbixAPI from config import conf +from datetime import datetime, timedelta + def get_active_problems() -> dict: api = ZabbixAPI(url=conf.zabbix.url, token=conf.zabbix.token) @@ -40,3 +42,39 @@ def get_active_problems() -> dict: return events_dict except: log.warning("Get event from zabbix error") + + +def event_acknowledge(event_id: int, mute_time: int): + api = ZabbixAPI(url=conf.zabbix.url, token=conf.zabbix.token) + if mute_time == 0: + mute_to = 0 + else: + mute_to = int((datetime.now() + timedelta(hours=mute_time)).timestamp()) + try: + response = api.event.acknowledge( + eventids=event_id, + action=34, + suppress_until=mute_to, + ) + if response: + log.info(f"Event {event_id} acknowledged") + return True + except: + log.warning(f"Acknowledge event {event_id} from zabbix error") + return False + + +def event_close(event_id: int): + api = ZabbixAPI(url=conf.zabbix.url, token=conf.zabbix.token) + try: + response = api.event.acknowledge( + eventids=event_id, + action=1, + ) + print(response) + if response: + log.info(f"Event {event_id} closed") + return True + except: + log.warning(f"Closed event {event_id} from zabbix error") + return False From f109bd59abab782e42a2345323e2c209abcf16f9 Mon Sep 17 00:00:00 2001 From: "s.mostryukov" Date: Thu, 13 Mar 2025 18:13:50 +0300 Subject: [PATCH 2/5] fix docker-compose --- docker/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index cab7fc2..f925ee5 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -9,7 +9,7 @@ services: command: [redis-server, --protected-mode yes, --port 6379, --requirepass, P@ssw0rd!] tg-bot: - image: git.sm8255082.ru/osnova/zbx-tg-bot:1.1.0 + image: git.sm8255082.ru/osnova/zbx-tg-bot:2.0.0 restart: always depends_on: - redis From 91d6b2045fc368ae8bcef160b682dbb00f5dd302 Mon Sep 17 00:00:00 2001 From: sergey Date: Thu, 13 Mar 2025 18:19:39 +0300 Subject: [PATCH 3/5] add aiogram in project --- pyproject.toml | 5 +++-- uv.lock | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ffd5c31..0c7b748 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,10 @@ [project] name = "zbx-tg-bot" -version = "0.1.0" -description = "Add your description here" +version = "2.0.0" +description = "telegram bot for telegram-zabbix dashboard" requires-python = ">=3.13" dependencies = [ + "aiogram>=3.18.0", "aiohttp>=3.11.13", "pydantic-settings>=2.8.1", "redis>=5.2.1", diff --git a/uv.lock b/uv.lock index a0f9795..a8cac40 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,32 @@ version = 1 revision = 1 requires-python = ">=3.13" +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, +] + +[[package]] +name = "aiogram" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "certifi" }, + { name = "magic-filter" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/18/a019fab03dca70d93e46ce5380254690415c2cbf3e084be003dc8c8b69ae/aiogram-3.18.0.tar.gz", hash = "sha256:429883a419751bfebeeafdc74804807d0abd5c9879ab0f06c045130de4752605", size = 1375474 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/17/cf1461c422815ad982daccbf636ec579f3bb178d4bdcb456f392478af70d/aiogram-3.18.0-py3-none-any.whl", hash = "sha256:ea2a2fbd11e4fffbba14a2081eb6322482ae569c6348618de5f7b6b41b52384d", size = 612779 }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -74,6 +100,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/33/7a7388b9ef94aab40539939d94461ec682afbd895458945ed25be07f03f6/attrs-25.2.0-py3-none-any.whl", hash = "sha256:611344ff0a5fed735d86d7784610c84f8126b95e549bcad9ff61b4242f2d386b", size = 64016 }, ] +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + [[package]] name = "frozenlist" version = "1.5.0" @@ -107,6 +142,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "magic-filter" +version = "1.0.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/08/da7c2cc7398cc0376e8da599d6330a437c01d3eace2f2365f300e0f3f758/magic_filter-1.0.12.tar.gz", hash = "sha256:4751d0b579a5045d1dc250625c4c508c18c3def5ea6afaf3957cb4530d03f7f9", size = 11071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/75/f620449f0056eff0ec7c1b1e088f71068eb4e47a46eb54f6c065c6ad7675/magic_filter-1.0.12-py3-none-any.whl", hash = "sha256:e5929e544f310c2b1f154318db8c5cdf544dd658efa998172acd2e4ba0f6c6a6", size = 11335 }, +] + [[package]] name = "multidict" version = "6.1.0" @@ -295,6 +339,7 @@ name = "zbx-tg-bot" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "aiogram" }, { name = "aiohttp" }, { name = "pydantic-settings" }, { name = "redis" }, @@ -303,6 +348,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "aiogram", specifier = ">=3.18.0" }, { name = "aiohttp", specifier = ">=3.11.13" }, { name = "pydantic-settings", specifier = ">=2.8.1" }, { name = "redis", specifier = ">=5.2.1" }, From be081cebfe57b29260a3c8979eae52c23d09ce40 Mon Sep 17 00:00:00 2001 From: "s.mostryukov" Date: Fri, 14 Mar 2025 11:28:11 +0300 Subject: [PATCH 4/5] add message --- telegram/bot.py | 17 ++++++++++++++--- zabbix/zabbix_api.py | 12 +++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 67695b0..3d911ac 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -13,7 +13,11 @@ dp = Dispatcher(storage=storage) @dp.callback_query(F.data.startswith("h")) async def handle_mute_1h(callback_query: types.CallbackQuery): - if event_acknowledge(int(callback_query.data[1:]), 1): + if event_acknowledge( + int(callback_query.data[1:]), + 1, + callback_query.from_user.username, + ): new_text = ( callback_query.message.text + "\n" @@ -25,7 +29,11 @@ async def handle_mute_1h(callback_query: types.CallbackQuery): @dp.callback_query(F.data.startswith("d")) async def handle_mute_1d(callback_query: types.CallbackQuery): - if event_acknowledge(int(callback_query.data[1:]), 24): + if event_acknowledge( + int(callback_query.data[1:]), + 24, + callback_query.from_user.username, + ): new_text = ( callback_query.message.text + "\n" @@ -37,7 +45,10 @@ async def handle_mute_1d(callback_query: types.CallbackQuery): @dp.callback_query(F.data.startswith("c")) async def handle_close(callback_query: types.CallbackQuery): - if event_close(int(callback_query.data[1:])): + if event_close( + int(callback_query.data[1:]), + callback_query.from_user.username, + ): new_text = ( callback_query.message.text + "\n" diff --git a/zabbix/zabbix_api.py b/zabbix/zabbix_api.py index 180c47b..44c6b2a 100644 --- a/zabbix/zabbix_api.py +++ b/zabbix/zabbix_api.py @@ -1,5 +1,6 @@ import logging as log +from pyexpat.errors import messages from zabbix_utils import ZabbixAPI from config import conf @@ -44,7 +45,7 @@ def get_active_problems() -> dict: log.warning("Get event from zabbix error") -def event_acknowledge(event_id: int, mute_time: int): +def event_acknowledge(event_id: int, mute_time: int, muted_by: str): api = ZabbixAPI(url=conf.zabbix.url, token=conf.zabbix.token) if mute_time == 0: mute_to = 0 @@ -53,8 +54,9 @@ def event_acknowledge(event_id: int, mute_time: int): try: response = api.event.acknowledge( eventids=event_id, - action=34, + action=38, suppress_until=mute_to, + message=muted_by, ) if response: log.info(f"Event {event_id} acknowledged") @@ -64,14 +66,14 @@ def event_acknowledge(event_id: int, mute_time: int): return False -def event_close(event_id: int): +def event_close(event_id: int, closed_by: str): api = ZabbixAPI(url=conf.zabbix.url, token=conf.zabbix.token) try: response = api.event.acknowledge( eventids=event_id, - action=1, + action=5, + message=closed_by, ) - print(response) if response: log.info(f"Event {event_id} closed") return True From a9edb5c4fc8f98c6c2bf7f49e95df0166de9ae00 Mon Sep 17 00:00:00 2001 From: sergey Date: Sun, 16 Mar 2025 21:51:42 +0300 Subject: [PATCH 5/5] upd readme --- README.md | 31 ++++++++++++++++++++++++++++++- docker/docker-compose.yaml | 1 + 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 023abaf..85e7216 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ -## Osnova telegram bot for zabbix \ No newline at end of file +## Osnova telegram bot for zabbix + +### Схема работы дашборда +1. Получает список актуальных алертов через api заббикс +2. Получает список отправленных сообщений из redis +3. Отправляет в чат сообщения об алертах, которых нет в reddis, но есть в актуальных алертов. +4. Удаляет из чата сообщения об алертах, которые есть в reddis, но нет в актуальных алертах. + +### Схема работы кнопок +Выполняет действие через api заббикс, в случае успеха, убирает кнопки. +В комментарий к мьюту\закрытию дописывает ник из телеги того кто закрыл. + +## Запуск. + +### Переменные +1. Строка запуска redis (или с паролем, или без пароля) +2. Уровень логирования в консоль и файл (30 - warning, 20 - info). + +3. url заббикс +4. token заббикс, с правами на чтение и мьют\закрытие алертов +5. Минимальный уровень алерта, которые будут отправляться в дашборд +6. Интервал опроса api заббикс в секундах + +7. Токен телеграм бота +8. ID чата для отправки сообщений +9. ID треда для отправки сообщений (0 для отправки в основной чат) + +10. Адрес для подключения к redis +11. Порт для подключения к redis +12. Пароль для подключения к redis (если нужен) \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index f925ee5..2dff12e 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -6,6 +6,7 @@ services: restart: always volumes: - ./local_redis_file/data:/data + #command: ["redis-server", --port 6379] command: [redis-server, --protected-mode yes, --port 6379, --requirepass, P@ssw0rd!] tg-bot: