Compare commits
23 Commits
Author | SHA1 | Date |
---|---|---|
|
2d9f8942b0 | |
|
605d086670 | |
|
6a03ac0e77 | |
|
9ac5c80f73 | |
|
876b3901a5 | |
|
09a890a04a | |
|
8b6d38563c | |
|
a5b6cdf9f7 | |
|
93e85f790e | |
|
ab2b4a40de | |
|
6c1b0c68e5 | |
|
672c02f64b | |
|
e9ee7bb8b0 | |
|
ac78933106 | |
|
12c124863e | |
|
3e34f4567c | |
|
a9edb5c4fc | |
|
be081cebfe | |
|
91d6b2045f | |
|
f109bd59ab | |
|
3ec90da1e0 | |
|
cc8fe1a17e | |
|
96929c25bc |
36
README.md
36
README.md
|
@ -1 +1,35 @@
|
||||||
## 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 (если нужен)
|
||||||
|
|
||||||
|
|
||||||
|
UPD
|
||||||
|
Добавлена проверка доступности апи заббикса и алерт в телегу, если не доступен.
|
||||||
|
Добавлено изменнеие алерта на произвольный текст, если сообщение нельзя удалить из-за срока давности.
|
||||||
|
|
|
@ -5,10 +5,16 @@ CFG__ZABBIX__URL=https://example.com
|
||||||
CFG__ZABBIX__TOKEN=string
|
CFG__ZABBIX__TOKEN=string
|
||||||
CFG__ZABBIX__MIN_SEVERITY=0
|
CFG__ZABBIX__MIN_SEVERITY=0
|
||||||
CFG__ZABBIX__UPD_INTERVAL=60
|
CFG__ZABBIX__UPD_INTERVAL=60
|
||||||
|
CFG__ZABBIX__ALERT_FAIL_COUNT=3
|
||||||
|
CFG__ZABBIX__ALERT_TREAD_ID=0
|
||||||
|
CFG__ZABBIX__ALERT_TAG_USER=string
|
||||||
|
CFG__ZABBIX__ALERT_TEXT_UP=Zabbix service UP
|
||||||
|
CFG__ZABBIX__ALERT_TEXT_DOWN=Zabbix service DOWN
|
||||||
|
|
||||||
CFG__TGBOT__TOKEN=string
|
CFG__TGBOT__TOKEN=string
|
||||||
CFG__TGBOT__CHAT_ID=00000000
|
CFG__TGBOT__CHAT_ID=00000000
|
||||||
CFG__TGBOT__TREAD_ID=0
|
CFG__TGBOT__TREAD_ID=0
|
||||||
|
CFG__TGBOT__DELETE_MSG=string
|
||||||
|
|
||||||
CFG__REDIS__HOST=localhost
|
CFG__REDIS__HOST=localhost
|
||||||
CFG__REDIS__PORT=6379
|
CFG__REDIS__PORT=6379
|
||||||
|
|
|
@ -42,6 +42,7 @@ class TelegramBotConfig(BaseModel):
|
||||||
token: str
|
token: str
|
||||||
chat_id: int
|
chat_id: int
|
||||||
tread_id: int
|
tread_id: int
|
||||||
|
delete_msg: str
|
||||||
|
|
||||||
|
|
||||||
class ZabbixConfig(BaseModel):
|
class ZabbixConfig(BaseModel):
|
||||||
|
@ -49,6 +50,13 @@ class ZabbixConfig(BaseModel):
|
||||||
token: str
|
token: str
|
||||||
min_severity: int
|
min_severity: int
|
||||||
upd_interval: int
|
upd_interval: int
|
||||||
|
alert_fail_count: int
|
||||||
|
alert_tread_id: int
|
||||||
|
alert_tag_user: str = ""
|
||||||
|
alert_text_up: str
|
||||||
|
alert_text_down: str
|
||||||
|
change_state_count: int = 0
|
||||||
|
api_state: str = "UP"
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
|
|
@ -6,11 +6,14 @@ services:
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./local_redis_file/data:/data
|
- ./local_redis_file/data:/data
|
||||||
|
#command: ["redis-server", --port 6379]
|
||||||
command: [redis-server, --protected-mode yes, --port 6379, --requirepass, P@ssw0rd!]
|
command: [redis-server, --protected-mode yes, --port 6379, --requirepass, P@ssw0rd!]
|
||||||
|
|
||||||
tg-bot:
|
tg-bot:
|
||||||
image: git.sm8255082.ru/osnova/zbx-tg-bot:1.0.0
|
image: git.sm8255082.ru/osnova/zbx-tg-bot:2.0.1
|
||||||
restart: always
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./logfile.log:/app/logfile.log
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
environment:
|
environment:
|
||||||
|
@ -21,10 +24,16 @@ services:
|
||||||
- CFG__ZABBIX__TOKEN=string
|
- CFG__ZABBIX__TOKEN=string
|
||||||
- CFG__ZABBIX__MIN_SEVERITY=2
|
- CFG__ZABBIX__MIN_SEVERITY=2
|
||||||
- CFG__ZABBIX__UPD_INTERVAL=30
|
- CFG__ZABBIX__UPD_INTERVAL=30
|
||||||
|
- CFG__ZABBIX__ALERT_FAIL_COUNT=3
|
||||||
|
- CFG__ZABBIX__ALERT_TREAD_ID=0
|
||||||
|
- CFG__ZABBIX__ALERT_TAG_USER=
|
||||||
|
- CFG__ZABBIX__ALERT_TEXT_UP=Zabbix service UP
|
||||||
|
- CFG__ZABBIX__ALERT_TEXT_DOWN=Zabbix service DOWN
|
||||||
|
|
||||||
- CFG__TGBOT__TOKEN=string
|
- CFG__TGBOT__TOKEN=string
|
||||||
- CFG__TGBOT__CHAT_ID=000000
|
- CFG__TGBOT__CHAT_ID=000000
|
||||||
- CFG__TGBOT__TREAD_ID=0
|
- CFG__TGBOT__TREAD_ID=0
|
||||||
|
- CFG__TGBOT__DELETE_MSG=Deleted
|
||||||
|
|
||||||
- CFG__REDIS__HOST=redis
|
- CFG__REDIS__HOST=redis
|
||||||
- CFG__REDIS__PORT=6379
|
- CFG__REDIS__PORT=6379
|
||||||
|
|
51
main.py
51
main.py
|
@ -1,7 +1,6 @@
|
||||||
import logging as log
|
import logging as log
|
||||||
from zabbix import get_active_problems
|
from zabbix import get_active_problems, check_state
|
||||||
from config import conf, icon_dict
|
from config import conf, icon_dict
|
||||||
from time import sleep
|
|
||||||
from redis_db import (
|
from redis_db import (
|
||||||
get_all_keys,
|
get_all_keys,
|
||||||
get_value,
|
get_value,
|
||||||
|
@ -9,13 +8,30 @@ from redis_db import (
|
||||||
del_value,
|
del_value,
|
||||||
)
|
)
|
||||||
import asyncio
|
import asyncio
|
||||||
from telegram import del_message, send_message
|
from telegram import (
|
||||||
|
del_message,
|
||||||
|
send_message,
|
||||||
|
start_bot,
|
||||||
|
send_zabbix_api_alert,
|
||||||
|
edit_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def main_loop():
|
async def dashboard():
|
||||||
active_alerts = get_active_problems()
|
active_alerts = get_active_problems()
|
||||||
if active_alerts is None:
|
if active_alerts is None:
|
||||||
|
if check_state(False) == "DOWN":
|
||||||
|
send_state = await send_zabbix_api_alert(conf.zabbix.alert_text_down)
|
||||||
|
if send_state["status"] == 200:
|
||||||
|
conf.zabbix.api_state = "DOWN"
|
||||||
|
conf.zabbix.change_state_count = 0
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
if check_state(True) == "UP":
|
||||||
|
send_state = await send_zabbix_api_alert(conf.zabbix.alert_text_up)
|
||||||
|
if send_state["status"] == 200:
|
||||||
|
conf.zabbix.api_state = "UP"
|
||||||
|
conf.zabbix.change_state_count = 0
|
||||||
telegram_alerts = await get_all_keys()
|
telegram_alerts = await get_all_keys()
|
||||||
|
|
||||||
new_alerts_id = []
|
new_alerts_id = []
|
||||||
|
@ -35,10 +51,10 @@ async def main_loop():
|
||||||
for new_alert in new_alerts_id:
|
for new_alert in new_alerts_id:
|
||||||
message = (
|
message = (
|
||||||
icon_dict[active_alerts[new_alert]["severity"]]
|
icon_dict[active_alerts[new_alert]["severity"]]
|
||||||
+ f"{active_alerts[new_alert]['host']}\n"
|
+ f" {active_alerts[new_alert]['host']}\n"
|
||||||
+ f"{active_alerts[new_alert]['name']}"
|
+ 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:
|
if msg_id["status"] == 200:
|
||||||
await set_value(key=new_alert, value=msg_id["msg_id"])
|
await set_value(key=new_alert, value=msg_id["msg_id"])
|
||||||
|
|
||||||
|
@ -49,16 +65,29 @@ async def main_loop():
|
||||||
if resp["status"] == 200:
|
if resp["status"] == 200:
|
||||||
await del_value(closed_alert)
|
await del_value(closed_alert)
|
||||||
if resp["status"] == 400:
|
if resp["status"] == 400:
|
||||||
log.warning(f"remove olg message {msg_id} from reddis")
|
resp = await edit_message(int(msg_id), conf.tgbot.delete_msg)
|
||||||
await del_value(closed_alert)
|
if resp["status"] == 200:
|
||||||
|
await del_value(closed_alert)
|
||||||
|
if resp["status"] == 400:
|
||||||
|
log.warning(f"remove olg message {msg_id} from reddis")
|
||||||
|
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__":
|
if __name__ == "__main__":
|
||||||
log.info("Starting app")
|
log.info("Starting app")
|
||||||
try:
|
try:
|
||||||
while True:
|
asyncio.run(main())
|
||||||
asyncio.run(main_loop())
|
|
||||||
sleep(conf.zabbix.upd_interval)
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log.info("Manual app stopped")
|
log.info("Manual app stopped")
|
||||||
log.info("App stopped")
|
log.info("App stopped")
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
[project]
|
[project]
|
||||||
name = "zbx-tg-bot"
|
name = "zbx-tg-bot"
|
||||||
version = "0.1.0"
|
version = "2.0.1"
|
||||||
description = "Add your description here"
|
description = "telegram bot for telegram-zabbix dashboard"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aiogram>=3.18.0",
|
||||||
"aiohttp>=3.11.13",
|
"aiohttp>=3.11.13",
|
||||||
"pydantic-settings>=2.8.1",
|
"pydantic-settings>=2.8.1",
|
||||||
"redis>=5.2.1",
|
"redis>=5.2.1",
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
from .message import (
|
from .message import (
|
||||||
send_message,
|
send_message,
|
||||||
|
edit_message,
|
||||||
del_message,
|
del_message,
|
||||||
|
send_zabbix_api_alert,
|
||||||
)
|
)
|
||||||
|
from .bot import start_bot
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"send_message",
|
"send_message",
|
||||||
|
"edit_message",
|
||||||
"del_message",
|
"del_message",
|
||||||
|
"start_bot",
|
||||||
|
"send_zabbix_api_alert",
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
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_close
|
||||||
|
from zabbix import event_acknowledge
|
||||||
|
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
|
import aiohttp
|
||||||
from config import conf
|
from config import conf
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
async def send_message(
|
async def send_message(message: str, event_id: int) -> dict:
|
||||||
message: str,
|
|
||||||
) -> dict:
|
|
||||||
url = f"https://api.telegram.org/bot{conf.tgbot.token}/sendMessage"
|
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 = {
|
params = {
|
||||||
"chat_id": conf.tgbot.chat_id,
|
"chat_id": conf.tgbot.chat_id,
|
||||||
"message_thread_id": conf.tgbot.tread_id,
|
"message_thread_id": conf.tgbot.tread_id,
|
||||||
"text": message,
|
"text": message,
|
||||||
|
"reply_markup": json.dumps(reply_markup),
|
||||||
}
|
}
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
|
@ -36,6 +45,35 @@ async def send_message(
|
||||||
return {"status": e}
|
return {"status": e}
|
||||||
|
|
||||||
|
|
||||||
|
async def edit_message(message_id: int, message: str) -> dict:
|
||||||
|
url = f"https://api.telegram.org/bot{conf.tgbot.token}/editMessageText"
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.post(
|
||||||
|
url,
|
||||||
|
json={
|
||||||
|
"chat_id": conf.tgbot.chat_id,
|
||||||
|
"message_id": message_id,
|
||||||
|
"text": message,
|
||||||
|
},
|
||||||
|
) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
log.info(f"Message ID {message_id} edited")
|
||||||
|
return {
|
||||||
|
"status": response.status,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
f"Message ID {message_id} NOT edit. Response status: {response.status}"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"status": response.status,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Exception: {e}")
|
||||||
|
return {"status": e}
|
||||||
|
|
||||||
|
|
||||||
async def del_message(
|
async def del_message(
|
||||||
message_id: int,
|
message_id: int,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
@ -58,9 +96,36 @@ async def del_message(
|
||||||
log.warning(
|
log.warning(
|
||||||
f"Message ID {message_id} NOT deleted. Response status: {response.status}"
|
f"Message ID {message_id} NOT deleted. Response status: {response.status}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": response.status,
|
"status": response.status,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"Exception: {e}")
|
log.warning(f"Exception: {e}")
|
||||||
return {"status": e}
|
return {"status": e}
|
||||||
|
|
||||||
|
|
||||||
|
async def send_zabbix_api_alert(message):
|
||||||
|
url = f"https://api.telegram.org/bot{conf.tgbot.token}/sendMessage"
|
||||||
|
params = {
|
||||||
|
"chat_id": conf.tgbot.chat_id,
|
||||||
|
"message_thread_id": conf.zabbix.alert_tread_id,
|
||||||
|
"text": conf.zabbix.alert_tag_user + "\n" + message,
|
||||||
|
}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.post(
|
||||||
|
url,
|
||||||
|
json=params,
|
||||||
|
) as response:
|
||||||
|
log.info(f"Response status: {response.status}")
|
||||||
|
resp = await response.json()
|
||||||
|
if response.status == 200:
|
||||||
|
log.info(f"Message with ID: {resp['result']['message_id']} send")
|
||||||
|
return {"status": response.status}
|
||||||
|
else:
|
||||||
|
log.warning(f"Message not send. Response status: {response.status}")
|
||||||
|
return {"status": response.status}
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Exception: {e}")
|
||||||
|
return {"status": e}
|
||||||
|
|
46
uv.lock
46
uv.lock
|
@ -2,6 +2,32 @@ version = 1
|
||||||
revision = 1
|
revision = 1
|
||||||
requires-python = ">=3.13"
|
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]]
|
[[package]]
|
||||||
name = "aiohappyeyeballs"
|
name = "aiohappyeyeballs"
|
||||||
version = "2.6.1"
|
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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "frozenlist"
|
name = "frozenlist"
|
||||||
version = "1.5.0"
|
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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.1.0"
|
version = "6.1.0"
|
||||||
|
@ -295,6 +339,7 @@ name = "zbx-tg-bot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "aiogram" },
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
|
@ -303,6 +348,7 @@ dependencies = [
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "aiogram", specifier = ">=3.18.0" },
|
||||||
{ name = "aiohttp", specifier = ">=3.11.13" },
|
{ name = "aiohttp", specifier = ">=3.11.13" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.8.1" },
|
{ name = "pydantic-settings", specifier = ">=2.8.1" },
|
||||||
{ name = "redis", specifier = ">=5.2.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_close, event_acknowledge, check_state
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"get_active_problems",
|
"get_active_problems",
|
||||||
|
"event_acknowledge",
|
||||||
|
"event_close",
|
||||||
|
"check_state",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import logging as log
|
import logging as log
|
||||||
|
|
||||||
|
from pyexpat.errors import messages
|
||||||
from zabbix_utils import ZabbixAPI
|
from zabbix_utils import ZabbixAPI
|
||||||
|
|
||||||
|
|
||||||
from config import conf
|
from config import conf
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
def get_active_problems() -> dict:
|
def get_active_problems() -> dict:
|
||||||
api = ZabbixAPI(url=conf.zabbix.url, token=conf.zabbix.token)
|
|
||||||
try:
|
try:
|
||||||
|
api = ZabbixAPI(url=conf.zabbix.url, token=conf.zabbix.token)
|
||||||
problems = api.problem.get(
|
problems = api.problem.get(
|
||||||
output=[
|
output=[
|
||||||
"eventid",
|
"eventid",
|
||||||
|
@ -24,7 +28,7 @@ def get_active_problems() -> dict:
|
||||||
event_ids.append(problem["eventid"])
|
event_ids.append(problem["eventid"])
|
||||||
|
|
||||||
events = api.event.get(
|
events = api.event.get(
|
||||||
selectHosts=["host"],
|
selectHosts=["name"],
|
||||||
eventids=event_ids,
|
eventids=event_ids,
|
||||||
output=[
|
output=[
|
||||||
"eventid",
|
"eventid",
|
||||||
|
@ -34,9 +38,73 @@ def get_active_problems() -> dict:
|
||||||
)
|
)
|
||||||
events_dict = {"event_ids": []}
|
events_dict = {"event_ids": []}
|
||||||
for event in events:
|
for event in events:
|
||||||
event["host"] = event.pop("hosts", None)[0]["host"]
|
event["host"] = event.pop("hosts", None)[0]["name"]
|
||||||
events_dict[event["eventid"]] = event
|
events_dict[event["eventid"]] = event
|
||||||
events_dict["event_ids"].append(event["eventid"])
|
events_dict["event_ids"].append(event["eventid"])
|
||||||
return events_dict
|
return events_dict
|
||||||
except:
|
except:
|
||||||
log.warning("Get event from zabbix error")
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def check_state(success: bool) -> str | None:
|
||||||
|
if success:
|
||||||
|
if conf.zabbix.api_state == "UP":
|
||||||
|
log.info("Zabbix API is UP")
|
||||||
|
conf.zabbix.change_state_count = 0
|
||||||
|
else:
|
||||||
|
if conf.zabbix.change_state_count == conf.zabbix.alert_fail_count:
|
||||||
|
log.warning(
|
||||||
|
f"Zabbix API state changed to UP. Count {conf.zabbix.change_state_count}"
|
||||||
|
)
|
||||||
|
return "UP"
|
||||||
|
else:
|
||||||
|
conf.zabbix.change_state_count += 1
|
||||||
|
else:
|
||||||
|
if conf.zabbix.api_state == "DOWN":
|
||||||
|
log.info("Zabbix API is DOWN")
|
||||||
|
conf.zabbix.change_state_count = 0
|
||||||
|
else:
|
||||||
|
if conf.zabbix.change_state_count == conf.zabbix.alert_fail_count:
|
||||||
|
log.warning(
|
||||||
|
f"Zabbix API state changed to DOWN. Count {conf.zabbix.change_state_count}"
|
||||||
|
)
|
||||||
|
return "DOWN"
|
||||||
|
else:
|
||||||
|
conf.zabbix.change_state_count += 1
|
||||||
|
|
Loading…
Reference in New Issue