dev-2.0.0 #3
			
				
			
		
		
		
	
							
								
								
									
										31
									
								
								README.md
								
								
								
								
							
							
						
						
									
										31
									
								
								README.md
								
								
								
								
							| 
						 | 
				
			
			@ -1 +1,30 @@
 | 
			
		|||
## Osnova telegram bot for zabbix
 | 
			
		||||
## 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 (если нужен)
 | 
			
		||||
| 
						 | 
				
			
			@ -6,10 +6,11 @@ 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:
 | 
			
		||||
    image: git.sm8255082.ru/osnova/zbx-tg-bot:1.0.0
 | 
			
		||||
    image: git.sm8255082.ru/osnova/zbx-tg-bot:2.0.0
 | 
			
		||||
    restart: always
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - redis
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										22
									
								
								main.py
								
								
								
								
							
							
						
						
									
										22
									
								
								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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,8 +2,10 @@ from .message import (
 | 
			
		|||
    send_message,
 | 
			
		||||
    del_message,
 | 
			
		||||
)
 | 
			
		||||
from .bot import start_bot
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    "send_message",
 | 
			
		||||
    "del_message",
 | 
			
		||||
    "start_bot",
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
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,
 | 
			
		||||
        callback_query.from_user.username,
 | 
			
		||||
    ):
 | 
			
		||||
        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,
 | 
			
		||||
        callback_query.from_user.username,
 | 
			
		||||
    ):
 | 
			
		||||
        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:]),
 | 
			
		||||
        callback_query.from_user.username,
 | 
			
		||||
    ):
 | 
			
		||||
        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)
 | 
			
		||||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										46
									
								
								uv.lock
								
								
								
								
							
							
						
						
									
										46
									
								
								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" },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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",
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,12 @@
 | 
			
		|||
import logging as log
 | 
			
		||||
 | 
			
		||||
from pyexpat.errors import messages
 | 
			
		||||
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 +43,40 @@ 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, muted_by: str):
 | 
			
		||||
    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=38,
 | 
			
		||||
            suppress_until=mute_to,
 | 
			
		||||
            message=muted_by,
 | 
			
		||||
        )
 | 
			
		||||
        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, closed_by: str):
 | 
			
		||||
    api = ZabbixAPI(url=conf.zabbix.url, token=conf.zabbix.token)
 | 
			
		||||
    try:
 | 
			
		||||
        response = api.event.acknowledge(
 | 
			
		||||
            eventids=event_id,
 | 
			
		||||
            action=5,
 | 
			
		||||
            message=closed_by,
 | 
			
		||||
        )
 | 
			
		||||
        if response:
 | 
			
		||||
            log.info(f"Event {event_id} closed")
 | 
			
		||||
        return True
 | 
			
		||||
    except:
 | 
			
		||||
        log.warning(f"Closed event {event_id} from zabbix error")
 | 
			
		||||
        return False
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue