Fix Vault integration and ALLOWED_USER_IDS handling
All checks were successful
Build and Deploy MikroTik Bot / build-and-deploy (push) Successful in 26s
All checks were successful
Build and Deploy MikroTik Bot / build-and-deploy (push) Successful in 26s
This commit is contained in:
parent
c2125e6736
commit
25d910cef7
BIN
__pycache__/vault_client.cpython-313.pyc
Normal file
BIN
__pycache__/vault_client.cpython-313.pyc
Normal file
Binary file not shown.
57
bot.py
57
bot.py
@ -9,29 +9,45 @@ from librouteros.exceptions import TrapError
|
|||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from db import init_db, upsert_client, start_session, update_session, close_session, get_history, get_clients, get_stats
|
from db import init_db, upsert_client, start_session, update_session, close_session, get_history, get_clients, get_stats
|
||||||
|
from vault_client import VaultClient
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN")
|
# Получаем конфигурацию из Vault или environment variables
|
||||||
MT_API_HOST = os.getenv("MT_API_HOST")
|
vault = VaultClient()
|
||||||
MT_API_USER = os.getenv("MT_API_USER")
|
config = vault.get_config()
|
||||||
MT_API_PASS = os.getenv("MT_API_PASS")
|
|
||||||
ALLOWED_USER_IDS = set(map(int, os.getenv("ALLOWED_USER_IDS", "").split(",")))
|
|
||||||
|
|
||||||
bot = Bot(token=TG_BOT_TOKEN)
|
# Используем правильные имена переменных
|
||||||
|
BOT_TOKEN = config.get('BOT_TOKEN') or os.getenv("BOT_TOKEN")
|
||||||
|
ROUTER_HOST = config.get('ROUTER_HOST') or os.getenv("ROUTER_HOST")
|
||||||
|
ROUTER_USER = config.get('ROUTER_USER') or os.getenv("ROUTER_USER")
|
||||||
|
ROUTER_PASSWORD = config.get('ROUTER_PASSWORD') or os.getenv("ROUTER_PASSWORD")
|
||||||
|
|
||||||
|
# Безопасная обработка ALLOWED_USER_IDS
|
||||||
|
allowed_ids_str = os.getenv("ALLOWED_USER_IDS", "")
|
||||||
|
if allowed_ids_str.strip():
|
||||||
|
ALLOWED_USER_IDS = set(map(int, allowed_ids_str.split(",")))
|
||||||
|
else:
|
||||||
|
# Если не установлено, используем пустое множество или значение по умолчанию
|
||||||
|
ALLOWED_USER_IDS = set()
|
||||||
|
print("⚠️ ALLOWED_USER_IDS не установлено - доступ для всех пользователей!")
|
||||||
|
|
||||||
|
bot = Bot(token=BOT_TOKEN)
|
||||||
dp = Dispatcher()
|
dp = Dispatcher()
|
||||||
|
|
||||||
# Авторизация по user_id
|
# Авторизация по user_id
|
||||||
async def check_user(message: types.Message) -> bool:
|
async def check_user(message: types.Message) -> bool:
|
||||||
|
# Если список пуст, разрешаем всем (для тестирования)
|
||||||
|
if not ALLOWED_USER_IDS:
|
||||||
|
return True
|
||||||
if message.from_user.id not in ALLOWED_USER_IDS:
|
if message.from_user.id not in ALLOWED_USER_IDS:
|
||||||
await message.answer("⛔️ Нет доступа")
|
await message.answer("⛔️ Нет доступа")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Подключение к MikroTik API
|
# Подключение к MikroTik API
|
||||||
|
|
||||||
def get_mt_api():
|
def get_mt_api():
|
||||||
return connect(username=MT_API_USER, password=MT_API_PASS, host=MT_API_HOST)
|
return connect(username=ROUTER_USER, password=ROUTER_PASSWORD, host=ROUTER_HOST)
|
||||||
|
|
||||||
@dp.message(Command("start"))
|
@dp.message(Command("start"))
|
||||||
async def start_cmd(msg: types.Message):
|
async def start_cmd(msg: types.Message):
|
||||||
@ -111,8 +127,10 @@ async def event(request: Request):
|
|||||||
# Сохраняем клиента и сессию (fetch)
|
# Сохраняем клиента и сессию (fetch)
|
||||||
await upsert_client(mac, name=None)
|
await upsert_client(mac, name=None)
|
||||||
await start_session(mac, ip, source='fetch')
|
await start_session(mac, ip, source='fetch')
|
||||||
for uid in ALLOWED_USER_IDS:
|
# Отправляем уведомления только если есть разрешённые пользователи
|
||||||
await bot.send_message(uid, text)
|
if ALLOWED_USER_IDS:
|
||||||
|
for uid in ALLOWED_USER_IDS:
|
||||||
|
await bot.send_message(uid, text)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
# --- Периодический polling MikroTik API для сбора статистики ---
|
# --- Периодический polling MikroTik API для сбора статистики ---
|
||||||
@ -167,8 +185,7 @@ async def history_cmd(msg: types.Message):
|
|||||||
|
|
||||||
@dp.callback_query(lambda c: c.data.startswith("history_"))
|
@dp.callback_query(lambda c: c.data.startswith("history_"))
|
||||||
async def history_period(callback: types.CallbackQuery):
|
async def history_period(callback: types.CallbackQuery):
|
||||||
if callback.from_user.id not in ALLOWED_USER_IDS:
|
if not await check_user_callback(callback):
|
||||||
await callback.answer("⛔️ Нет доступа", show_alert=True)
|
|
||||||
return
|
return
|
||||||
parts = callback.data.split("_")
|
parts = callback.data.split("_")
|
||||||
days = int(parts[1])
|
days = int(parts[1])
|
||||||
@ -219,8 +236,7 @@ async def clients_cmd(msg: types.Message):
|
|||||||
|
|
||||||
@dp.callback_query(lambda c: c.data.startswith("clients_"))
|
@dp.callback_query(lambda c: c.data.startswith("clients_"))
|
||||||
async def clients_page_cb(callback: types.CallbackQuery):
|
async def clients_page_cb(callback: types.CallbackQuery):
|
||||||
if callback.from_user.id not in ALLOWED_USER_IDS:
|
if not await check_user_callback(callback):
|
||||||
await callback.answer("⛔️ Нет доступа", show_alert=True)
|
|
||||||
return
|
return
|
||||||
parts = callback.data.split("_")
|
parts = callback.data.split("_")
|
||||||
page = int(parts[1]) if len(parts) > 1 else 1
|
page = int(parts[1]) if len(parts) > 1 else 1
|
||||||
@ -272,8 +288,7 @@ async def stats_cmd(msg: types.Message):
|
|||||||
|
|
||||||
@dp.callback_query(lambda c: c.data.startswith("stats_"))
|
@dp.callback_query(lambda c: c.data.startswith("stats_"))
|
||||||
async def stats_period(callback: types.CallbackQuery):
|
async def stats_period(callback: types.CallbackQuery):
|
||||||
if callback.from_user.id not in ALLOWED_USER_IDS:
|
if not await check_user_callback(callback):
|
||||||
await callback.answer("⛔️ Нет доступа", show_alert=True)
|
|
||||||
return
|
return
|
||||||
parts = callback.data.split("_")
|
parts = callback.data.split("_")
|
||||||
days = int(parts[1])
|
days = int(parts[1])
|
||||||
@ -313,5 +328,15 @@ async def send_stats_page(message, period_days, page, edit=False, callback=None)
|
|||||||
else:
|
else:
|
||||||
await message.answer(text, parse_mode="HTML", reply_markup=kb)
|
await message.answer(text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
|
||||||
|
# Добавляем вспомогательную функцию для проверки callback'ов
|
||||||
|
async def check_user_callback(callback: types.CallbackQuery) -> bool:
|
||||||
|
# Если список пуст, разрешаем всем
|
||||||
|
if not ALLOWED_USER_IDS:
|
||||||
|
return True
|
||||||
|
if callback.from_user.id not in ALLOWED_USER_IDS:
|
||||||
|
await callback.answer("⛔️ Нет доступа", show_alert=True)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
@ -1,44 +1,44 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
services:
|
|
||||||
telegram-bot:
|
|
||||||
build: .
|
|
||||||
container_name: mikrotik-telegram-bot
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ./data:/app/data
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro # для watchtower
|
|
||||||
labels:
|
|
||||||
- "com.centurylinklabs.watchtower.enable=true"
|
|
||||||
- "com.centurylinklabs.watchtower.scope=mikrotik-bot"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 15s
|
|
||||||
networks:
|
|
||||||
- mikrotik-net
|
|
||||||
|
|
||||||
# Автообновление контейнеров при изменении кода
|
services:
|
||||||
watchtower:
|
mikrotik-bot:
|
||||||
image: containrrr/watchtower:latest
|
image: 10.10.30.121:5000/mikrotik-bot:latest
|
||||||
container_name: watchtower-mikrotik
|
container_name: mikrotik-bot-production
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- WATCHTOWER_CLEANUP=true
|
# Vault AppRole credentials (безопасно)
|
||||||
- WATCHTOWER_POLL_INTERVAL=300 # проверка каждые 5 мин
|
- VAULT_ADDR=http://10.10.30.121:8200
|
||||||
- WATCHTOWER_SCOPE=mikrotik-bot
|
- VAULT_ROLE_ID=ba8d3d21-263e-4d92-8ffe-ef803017cef5
|
||||||
- WATCHTOWER_INCLUDE_STOPPED=true
|
- VAULT_SECRET_ID=6b3ecc3c-9436-7f04-022f-8b1ce0ac09ee
|
||||||
- WATCHTOWER_REVIVE_STOPPED=true
|
- VAULT_SECRET_PATH=secret/data/mikrotik-bot
|
||||||
|
- DATABASE_PATH=/app/data/bot.db
|
||||||
|
volumes:
|
||||||
|
- mikrotik_bot_data:/app/data
|
||||||
|
ports:
|
||||||
|
- "8001:8000" # Health check endpoint
|
||||||
|
networks:
|
||||||
|
- bot-network
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
|
|
||||||
|
# Автоматические обновления
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
container_name: watchtower-mikrotik
|
||||||
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
labels:
|
environment:
|
||||||
- "com.centurylinklabs.watchtower.scope=mikrotik-bot"
|
- WATCHTOWER_POLL_INTERVAL=60 # Проверять каждые 60 секунд
|
||||||
|
- WATCHTOWER_LABEL_ENABLE=true
|
||||||
|
- WATCHTOWER_CLEANUP=true
|
||||||
|
command: --interval 60 --label-enable --cleanup
|
||||||
networks:
|
networks:
|
||||||
- mikrotik-net
|
- bot-network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
mikrotik-net:
|
bot-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mikrotik_bot_data:
|
||||||
46
docker-compose.production.yml
Normal file
46
docker-compose.production.yml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mikrotik-bot:
|
||||||
|
image: 10.10.30.121:5000/mikrotik-bot:latest
|
||||||
|
container_name: mikrotik-bot-production
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# Vault AppRole credentials (безопасно)
|
||||||
|
- VAULT_ADDR=http://10.10.30.121:8200
|
||||||
|
- VAULT_ROLE_ID=ba8d3d21-263e-4d92-8ffe-ef803017cef5
|
||||||
|
- VAULT_SECRET_ID=6b3ecc3c-9436-7f04-022f-8b1ce0ac09ee
|
||||||
|
- VAULT_SECRET_PATH=secret/data/mikrotik-bot
|
||||||
|
- DATABASE_PATH=/app/data/bot.db
|
||||||
|
volumes:
|
||||||
|
- mikrotik_bot_data:/app/data
|
||||||
|
ports:
|
||||||
|
- "8000:8000" # Health check endpoint
|
||||||
|
networks:
|
||||||
|
- bot-network
|
||||||
|
depends_on:
|
||||||
|
- watchtower
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
|
|
||||||
|
# Автоматические обновления
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
container_name: watchtower-mikrotik
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- WATCHTOWER_POLL_INTERVAL=60 # Проверять каждые 60 секунд
|
||||||
|
- WATCHTOWER_LABEL_ENABLE=true
|
||||||
|
- WATCHTOWER_CLEANUP=true
|
||||||
|
command: --interval 60 --label-enable --cleanup
|
||||||
|
networks:
|
||||||
|
- bot-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
bot-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mikrotik_bot_data:
|
||||||
40
test_vault.py
Normal file
40
test_vault.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from vault_client import VaultClient
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Устанавливаем переменные окружения для тестирования
|
||||||
|
os.environ['VAULT_ADDR'] = 'http://10.10.30.121:8200'
|
||||||
|
os.environ['VAULT_ROLE_ID'] = 'ba8d3d21-263e-4d92-8ffe-ef803017cef5'
|
||||||
|
os.environ['VAULT_SECRET_ID'] = '6b3ecc3c-9436-7f04-022f-8b1ce0ac09ee'
|
||||||
|
os.environ['VAULT_SECRET_PATH'] = 'secret/data/mikrotik-bot'
|
||||||
|
|
||||||
|
def test_vault_secrets():
|
||||||
|
"""Тестирование получения секретов из Vault"""
|
||||||
|
print('🔍 Проверка получения секретов из Vault:')
|
||||||
|
print('=' * 50)
|
||||||
|
|
||||||
|
vault = VaultClient()
|
||||||
|
config = vault.get_config()
|
||||||
|
|
||||||
|
if not config:
|
||||||
|
print('❌ Не удалось получить конфигурацию')
|
||||||
|
return False
|
||||||
|
|
||||||
|
success = True
|
||||||
|
for key, value in config.items():
|
||||||
|
if value:
|
||||||
|
masked_value = value[:10] + '...' if len(value) > 10 else value
|
||||||
|
print(f'✅ {key}: {masked_value}')
|
||||||
|
else:
|
||||||
|
print(f'❌ {key}: не найден')
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if test_vault_secrets():
|
||||||
|
print('\n🎉 Все секреты успешно получены!')
|
||||||
|
print('✅ Готов к production deployment')
|
||||||
|
else:
|
||||||
|
print('\n❌ Есть проблемы с получением секретов')
|
||||||
84
vault_client.py
Normal file
84
vault_client.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class VaultClient:
|
||||||
|
"""Клиент для работы с HashiCorp Vault через AppRole аутентификацию"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.vault_addr = os.environ.get('VAULT_ADDR', 'http://localhost:8200')
|
||||||
|
self.role_id = os.environ.get('VAULT_ROLE_ID')
|
||||||
|
self.secret_id = os.environ.get('VAULT_SECRET_ID')
|
||||||
|
self.secret_path = os.environ.get('VAULT_SECRET_PATH', 'secret/data/mikrotik-bot')
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def authenticate(self) -> bool:
|
||||||
|
"""Аутентификация через AppRole"""
|
||||||
|
if not self.role_id or not self.secret_id:
|
||||||
|
print("❌ VAULT_ROLE_ID или VAULT_SECRET_ID не установлены")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth_url = f"{self.vault_addr}/v1/auth/approle/login"
|
||||||
|
auth_data = {
|
||||||
|
"role_id": self.role_id,
|
||||||
|
"secret_id": self.secret_id
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(auth_url, json=auth_data)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
auth_result = response.json()
|
||||||
|
self.token = auth_result['auth']['client_token']
|
||||||
|
print("✅ Vault аутентификация успешна")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка аутентификации Vault: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_secrets(self) -> Optional[Dict[str, str]]:
|
||||||
|
"""Получение секретов из Vault"""
|
||||||
|
if not self.token and not self.authenticate():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
headers = {"X-Vault-Token": self.token}
|
||||||
|
secret_url = f"{self.vault_addr}/v1/{self.secret_path}"
|
||||||
|
|
||||||
|
response = requests.get(secret_url, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
secret_data = response.json()
|
||||||
|
secrets = secret_data['data']['data']
|
||||||
|
|
||||||
|
print("✅ Секреты успешно получены из Vault")
|
||||||
|
return secrets
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка получения секретов: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_config(self) -> Dict[str, str]:
|
||||||
|
"""Получение конфигурации с fallback на environment variables"""
|
||||||
|
# Сначала пытаемся получить из Vault
|
||||||
|
secrets = self.get_secrets()
|
||||||
|
|
||||||
|
if secrets:
|
||||||
|
return {
|
||||||
|
'BOT_TOKEN': secrets.get('bot_token'),
|
||||||
|
'ROUTER_HOST': secrets.get('router_host'),
|
||||||
|
'ROUTER_USER': secrets.get('router_user'),
|
||||||
|
'ROUTER_PASSWORD': secrets.get('router_password'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback на environment variables
|
||||||
|
print("⚠️ Используются environment variables вместо Vault")
|
||||||
|
return {
|
||||||
|
'BOT_TOKEN': os.environ.get('BOT_TOKEN'),
|
||||||
|
'ROUTER_HOST': os.environ.get('ROUTER_HOST'),
|
||||||
|
'ROUTER_USER': os.environ.get('ROUTER_USER'),
|
||||||
|
'ROUTER_PASSWORD': os.environ.get('ROUTER_PASSWORD'),
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user