Add MikroTik bot source code and Dockerfile

This commit is contained in:
stakost 2025-06-01 11:36:06 +03:00
parent 428d5afacb
commit 521bb90611
25 changed files with 2533 additions and 1 deletions

83
AI_CONTEXT.md Normal file
View File

@ -0,0 +1,83 @@
# AI_CONTEXT: MikroTik Telegram Bot
## ARCH:CORE
- **Lang**: Python 3.10+
- **Stack**: aiogram3+librouteros+fastapi+aiosqlite
- **Deploy**: Docker/bare
- **Auth**: TG_user_id whitelist
## FUNCS:MAIN
1. **WiFi_Monitor**: /clients, live_polling_30s, pagination
2. **History**: /history, sessions_DB, date_filters
3. **Stats**: /stats, rx/tx_aggregation, top_users
4. **Events**: FastAPI_webhook + polling_hybrid
## FILES:KEY
- `bot.py`: main_logic, TG_handlers, API_polling
- `db.py`: aiosqlite_schema, CRUD_ops
- `requirements.txt`: deps_list
- `docker-compose.yml`: container_config
- `docker-compose.portainer.yml`: watchtower_autodeploy
- `DEPLOY.md`: portainer_setup_instructions
## API:MT_PATHS
- **WiFi_clients**: `/interface/wifi/registration-table` (RouterOS7+)
- **Fields**: mac-address, interface, ssid, uptime, signal, band
## DB:SCHEMA
```sql
clients(mac PK, name, custom_name, last_seen)
sessions(id, mac FK, ip, start/end_time, rx/tx_bytes, source)
```
## ENVS:REQ
```
TG_BOT_TOKEN=bot_token
MT_API_HOST=router_ip
MT_API_USER=api_user
MT_API_PASS=api_pass
ALLOWED_USER_IDS=123,456
```
## DEPLOY:AUTODEPLOY
**Method1:Watchtower** (recommended)
- docker-compose.portainer.yml → portainer_stack
- watchtower checks updates every 5min
- auto rebuild+restart on code changes
**Method2:GitHub_Registry**
- .github/workflows/docker.yml → ghcr.io
- webhook triggers portainer update
- requires public repo or PAT
## DEPLOY:QUICK
```bash
docker build -t mt-bot .
docker run --env-file .env mt-bot
```
## CMDS:TG
- `/start` → main_menu
- `/clients` → wifi_list_paginated
- `/history [days]` → sessions_history
- `/stats [days]` → traffic_stats
## ENDPOINTS:API
- `GET /health` → healthcheck
- `GET /event?mac=XX&ip=XX&type=XX` → webhook
## POLL:LOGIC
- 30s_interval MT_API_check
- upsert_clients → track_sessions
- close_sessions_for_disconnected
## WEBHOOK:FETCH
- GET `/event?mac=XX&ip=XX&type=XX`
- saves_to_DB + notifies_TG_users
## NOTES:DEV
- RouterOS7 fields differ from v6
- no rx/tx in wifi_registration_table
- polling+webhook hybrid for reliability
- pagination: 10items/page, nav_buttons
- healthcheck: curl localhost:8000/health

58
DEPLOY.md Normal file
View File

@ -0,0 +1,58 @@
# Автодеплой в Portainer
## Вариант 1: Watchtower (рекомендуемый)
### Шаги:
1. В Portainer создай Stack с именем `mikrotik-bot`
2. Скопируй содержимое `docker-compose.portainer.yml`
3. Создай `.env` файл с переменными:
```
TG_BOT_TOKEN=your_bot_token
MT_API_HOST=192.168.1.1
MT_API_USER=admin
MT_API_PASS=password
ALLOWED_USER_IDS=123456,789012
```
4. Deploy Stack
### Как работает:
- Watchtower проверяет обновления каждые 5 минут
- При изменении кода rebuilds образ автоматически
- Чистит старые образы после обновления
## Вариант 2: GitHub Registry + Webhook
### Настройка GitHub:
1. Включи GitHub Actions в репозитории
2. Push код - автоматически собирется образ в ghcr.io
3. В `docker-compose.registry.yml` замени:
```yaml
image: ghcr.io/YOUR_USERNAME/mikrotik-telegram-bot:latest
```
### Настройка Portainer:
1. Создай Stack с `docker-compose.registry.yml`
2. В настройках Stack включи "Auto Update"
3. Создай Webhook для автообновления
### Webhook URL:
```
https://portainer.domain.com/api/webhooks/YOUR_WEBHOOK_KEY
```
## Проверка работы:
- Health check: `curl http://localhost:8000/health`
- Логи: `docker logs mikrotik-telegram-bot`
- Статус: Portainer → Stacks → mikrotik-bot
## Откат:
Если что-то сломалось:
```bash
docker-compose down
docker-compose up -d
```
## Мониторинг:
- Portainer показывает статус health check
- Telegram бот отправляет уведомления при ошибках
- FastAPI endpoint `/health` для внешнего мониторинга

12
Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python -c "import db; db.init_db()"
CMD ["python", "bot.py"]

240
INFRA_PLAN.md Normal file
View File

@ -0,0 +1,240 @@
# CI/CD Infrastructure Plan - Proxmox
## ARCH:OVERVIEW
```
[DEV] → [Git Server] → [CI/CD] → [Registry] → [Portainer] → [MikroTik Bot]
```
## QUICK:START
```bash
cd infra/
chmod +x *.sh
./deploy-all.sh
```
**Результат:** Полная CI/CD инфраструктура за 10 минут!
## VMs/LXC:SETUP
### 1. Git Server (LXC)
- **OS**: Ubuntu 22.04 LXC
- **Memory**: 2GB RAM, 20GB disk
- **Service**: Gitea (lightweight GitHub alternative)
- **Features**:
- Web UI для управления репозиториями
- Webhook support для CI/CD
- Built-in Actions (CI/CD)
- Issues, PRs, Wiki
### 2. CI/CD + Registry (LXC)
- **OS**: Ubuntu 22.04 LXC
- **Memory**: 4GB RAM, 50GB disk
- **Services**:
- GitLab Runner / Gitea Actions Runner
- Docker Registry (private)
- Nexus/Harbor (опционально)
### 3. Production Environment (LXC)
- **OS**: Ubuntu 22.04 LXC
- **Memory**: 2GB RAM, 20GB disk
- **Services**:
- Portainer CE
- MikroTik Telegram Bot
- Watchtower (автообновление)
## NETWORK:CONFIG
```
Proxmox Bridge (vmbr1): 10.10.0.0/24
├── Git Server: 10.10.0.10
├── CI/CD Registry: 10.10.0.20
├── Production: 10.10.0.30
└── Gateway: 10.10.0.1 (access from home LAN)
```
## SERVICES:STACK
### Git Server (Gitea)
```yaml
version: '3.8'
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=git.home.lab
- GITEA__server__ROOT_URL=http://git.home.lab:3000
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
restart: unless-stopped
```
### CI/CD + Registry
```yaml
version: '3.8'
services:
# Private Docker Registry
registry:
image: registry:2
container_name: docker-registry
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
volumes:
- registry_data:/data
ports:
- "5000:5000"
restart: unless-stopped
# Gitea Actions Runner
runner:
image: gitea/act_runner:latest
container_name: gitea-runner
environment:
GITEA_INSTANCE_URL: http://10.10.0.10:3000
GITEA_RUNNER_REGISTRATION_TOKEN: "your_token"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- runner_data:/data
restart: unless-stopped
```
### Production (Portainer + Apps)
```yaml
version: '3.8'
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
command: -H unix:///var/run/docker.sock
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
ports:
- "9000:9000"
restart: unless-stopped
```
## WORKFLOW:CI_CD
### 1. Development Flow
```
git push → Gitea → webhook → Actions → build → registry → webhook → Portainer → deploy
```
### 2. Gitea Actions Workflow (.gitea/workflows/deploy.yml)
```yaml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Image
run: |
docker build -t 10.10.0.20:5000/mikrotik-bot:${{ github.sha }} .
docker push 10.10.0.20:5000/mikrotik-bot:${{ github.sha }}
docker tag 10.10.0.20:5000/mikrotik-bot:${{ github.sha }} 10.10.0.20:5000/mikrotik-bot:latest
docker push 10.10.0.20:5000/mikrotik-bot:latest
- name: Deploy to Production
run: |
curl -X POST "http://10.10.0.30:9000/api/webhooks/YOUR_WEBHOOK_KEY"
```
## DEPLOY:STEPS
### Phase 1: Infrastructure Setup
1. **Create LXC containers** in Proxmox
2. **Network configuration** (vmbr1 bridge)
3. **Install Docker** on all containers
4. **Configure DNS** (local or Pi-hole): git.home.lab, registry.home.lab, portainer.home.lab
### Phase 2: Services Deployment
1. **Deploy Gitea** (10.10.0.10:3000)
2. **Create mikrotik-bot repository**
3. **Deploy Registry** (10.10.0.20:5000)
4. **Setup Gitea Actions Runner**
5. **Deploy Portainer** (10.10.0.30:9000)
### Phase 3: CI/CD Configuration
1. **Configure Gitea webhook** → Actions
2. **Setup registry access** (insecure for local)
3. **Create Portainer webhook** for auto-deploy
4. **Test full pipeline**
### Phase 4: Production Deploy
1. **Push code** to Gitea
2. **Verify automatic build** in registry
3. **Confirm auto-deploy** to Portainer
4. **Setup monitoring** and alerts
## SECURITY:CONSIDERATIONS
- **Registry**: HTTP (internal network only)
- **Gitea**: HTTP (можно добавить HTTPS с self-signed)
- **Firewall**: только нужные порты
- **Backup**: регулярные снапшоты LXC
- **Access**: VPN или закрытая сеть
## MONITORING:STACK (Optional)
```yaml
# Prometheus + Grafana
prometheus:
image: prom/prometheus:latest
ports: ["9090:9090"]
grafana:
image: grafana/grafana:latest
ports: ["3001:3000"]
# Log aggregation
loki:
image: grafana/loki:latest
ports: ["3100:3100"]
```
## RESOURCES:REQUIREMENTS
- **Total RAM**: 8GB (2+4+2)
- **Total Disk**: 90GB (20+50+20)
- **Network**: 1Gbps internal
- **Backup**: еженедельные снапшоты
## BENEFITS:SELFHOSTED
✅ Полный контроль над кодом и инфраструктурой
✅ Нет зависимости от внешних сервисов
✅ Быстрая локальная сеть (Gigabit)
✅ Бесплатно (кроме электричества)
✅ Легкое масштабирование в Proxmox
✅ Интеграция с домашней сетью
## MIGRATION:PATH
Existing code → Gitea → Actions → Registry → Portainer
## FILES:CREATED
```
infra/
├── setup-lxc.sh # Автоматическое создание LXC
├── deploy-all.sh # Полное развертывание
├── gitea-compose.yml # Git сервер + DB
├── registry-compose.yml # Registry + UI + Watchtower
├── production-compose.yml # Portainer + Apps
└── README.md # Подробные инструкции
.gitea/workflows/
└── deploy.yml # CI/CD pipeline
```
## USAGE:SUMMARY
1. **Один скрипт** развертывает всю инфраструктуру
2. **Настройка** заняет 15-20 минут
3. **Push код** → автоматический деплой
4. **Мониторинг** через веб интерфейсы
5. **Масштабирование** добавлением новых LXC

View File

@ -1,2 +1,35 @@
# mikrotik-bot
# Telegram-бот для мониторинга и управления MikroTik
## Описание
Бот для Telegram, который позволяет:
- Получать уведомления о новых Wi-Fi-подключениях, пропадании интернета, статистике
- Управлять устройствами (VLAN, блокировка, просмотр клиентов)
- Работать только через RouterOS API (минимум fetch-скриптов)
## Архитектура
- Python 3.10+
- aiogram (Telegram-бот)
- librouteros (интеграция с MikroTik API)
- Docker или virtualenv для изоляции
## Быстрый старт (Docker)
1. Клонируй репозиторий или скопируй папку
2. Заполни .env (пример см. .env.example)
3. Собери и запусти контейнер:
```
docker build -t mikrotik-telegram-bot .
docker run --env-file .env --restart unless-stopped mikrotik-telegram-bot
```
4. Для автоматического обновления через Portainer используй Watchtower или настрой auto-pull (если репозиторий в GitHub/GitLab).
## Быстрый старт (без Docker)
1. pip install -r requirements.txt
2. Заполни .env
3. python bot.py
## Безопасность
- Токен Telegram и доступ к MikroTik храни только в .env
- Ограничь доступ к боту по user_id
## Документация
- Все примеры и расширения смотри в файлах mikrotik_home/7step_telegram_bot.md и bot.py

317
bot.py Normal file
View File

@ -0,0 +1,317 @@
import os
import asyncio
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from dotenv import load_dotenv
from librouteros import connect
from librouteros.exceptions import TrapError
from fastapi import FastAPI, Request
import uvicorn
from db import init_db, upsert_client, start_session, update_session, close_session, get_history, get_clients, get_stats
load_dotenv()
TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN")
MT_API_HOST = os.getenv("MT_API_HOST")
MT_API_USER = os.getenv("MT_API_USER")
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)
dp = Dispatcher()
# Авторизация по user_id
async def check_user(message: types.Message) -> bool:
if message.from_user.id not in ALLOWED_USER_IDS:
await message.answer("⛔️ Нет доступа")
return False
return True
# Подключение к MikroTik API
def get_mt_api():
return connect(username=MT_API_USER, password=MT_API_PASS, host=MT_API_HOST)
@dp.message(Command("start"))
async def start_cmd(msg: types.Message):
if not await check_user(msg):
return
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Клиенты Wi-Fi", callback_data="clients")]
])
await msg.answer("Добро пожаловать!", reply_markup=kb)
@dp.callback_query(lambda c: c.data == "clients")
async def show_clients(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS:
await callback.answer("⛔️ Нет доступа", show_alert=True)
return
try:
print("[DEBUG] Подключаюсь к MikroTik API...")
api = get_mt_api()
print("[DEBUG] Получаю список клиентов через /interface/wifi/registration-table...")
# Правильный путь для RouterOS 7.x
clients = list(api.path('interface', 'wifi', 'registration-table'))
print(f"[DEBUG] Найдено клиентов: {len(clients)}")
if not clients:
await callback.message.answer("Нет подключённых клиентов Wi-Fi.")
return
text = "<b>Wi-Fi клиенты:</b>\n"
for i, c in enumerate(clients):
print(f"[DEBUG] Обрабатываю клиента {i}: type={type(c)}")
# Конвертируем Path объект в словарь
client_data = dict(c)
print(f"[DEBUG] Данные клиента: {client_data}")
# Используем правильные поля из RouterOS 7.x
mac = client_data.get('mac-address', '-')
interface = client_data.get('interface', '-')
ssid = client_data.get('ssid', '-')
uptime = client_data.get('uptime', '-')
signal = client_data.get('signal', '-')
band = client_data.get('band', '-')
text += f"\n<b>MAC:</b> {mac}\n"
text += f"<b>Интерфейс:</b> {interface}\n"
text += f"<b>SSID:</b> {ssid}\n"
text += f"<b>Uptime:</b> {uptime}\n"
text += f"<b>Сигнал:</b> {signal} | <b>Диапазон:</b> {band}\n"
print("[DEBUG] Отправляю сообщение...")
await callback.message.answer(text, parse_mode="HTML")
except TrapError as e:
print(f"[ERROR] TrapError: {e}")
await callback.message.answer(f"Ошибка API MikroTik: {e}")
except Exception as e:
print(f"[ERROR] Exception: {e}")
print(f"[ERROR] Exception type: {type(e)}")
import traceback
traceback.print_exc()
await callback.message.answer(f"Ошибка: {e}")
# --- FastAPI для fetch-уведомлений ---
app = FastAPI()
@app.get("/health")
async def health():
return {"status": "ok", "service": "mikrotik-telegram-bot"}
@app.get("/event")
async def event(request: Request):
mac = request.query_params.get('mac')
ip = request.query_params.get('ip')
event_type = request.query_params.get('type')
# Можно добавить секрет для безопасности:
# if request.query_params.get('secret') != os.getenv('MT_SECRET'): return 'forbidden'
text = f"MikroTik: событие {event_type}\nMAC: {mac}\nIP: {ip}"
# Сохраняем клиента и сессию (fetch)
await upsert_client(mac, name=None)
await start_session(mac, ip, source='fetch')
for uid in ALLOWED_USER_IDS:
await bot.send_message(uid, text)
return {"status": "ok"}
# --- Периодический polling MikroTik API для сбора статистики ---
async def poll_mikrotik():
known_clients = {}
while True:
try:
api = get_mt_api()
# Используем правильный путь для RouterOS 7.x
clients = list(api.path('interface', 'wifi', 'registration-table'))
current_macs = set()
for c in clients:
# Конвертируем Path объект в словарь
client_data = dict(c)
mac = client_data.get('mac-address')
# В RouterOS 7.x WiFi нет поля last-ip, используем другие поля
ip = client_data.get('last-ip', 'N/A') # возможно нет этого поля
name = client_data.get('host-name') or client_data.get('comment')
# В WiFi registration-table нет rx-bytes/tx-bytes, используем 0
rx = 0
tx = 0
await upsert_client(mac, name=name)
await start_session(mac, ip, rx, tx, source='api')
await update_session(mac, rx, tx)
current_macs.add(mac)
# Закрываем сессии для клиентов, которых больше нет
for mac in list(known_clients):
if mac not in current_macs:
await close_session(mac)
known_clients.pop(mac)
known_clients = {mac: True for mac in current_macs}
except Exception as e:
print(f"[poll_mikrotik] Ошибка: {e}")
await asyncio.sleep(30) # интервал опроса
# --- Одновременный запуск бота, FastAPI и polling ---
async def main():
await init_db()
bot_task = asyncio.create_task(dp.start_polling(bot))
config = uvicorn.Config(app, host="0.0.0.0", port=8000, log_level="info")
server = uvicorn.Server(config)
api_task = asyncio.create_task(server.serve())
poll_task = asyncio.create_task(poll_mikrotik())
await asyncio.gather(bot_task, api_task, poll_task)
# --- Команда /history с постраничностью ---
@dp.message(Command("history"))
async def history_cmd(msg: types.Message):
if not await check_user(msg):
return
await send_history_page(msg, period_days=30, page=1)
@dp.callback_query(lambda c: c.data.startswith("history_"))
async def history_period(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS:
await callback.answer("⛔️ Нет доступа", show_alert=True)
return
parts = callback.data.split("_")
days = int(parts[1])
page = int(parts[2]) if len(parts) > 2 else 1
await send_history_page(callback.message, period_days=days, page=page, edit=True, callback=callback)
async def send_history_page(message, period_days, page, edit=False, callback=None):
page_size = 10
rows, total = await get_history(period_days=period_days, page=page, page_size=page_size)
max_page = max(1, (total + page_size - 1) // page_size)
text = f"<b>История подключений за {period_days} дн. (стр. {page}/{max_page}):</b>\n"
if not rows:
text += "Нет подключений за выбранный период."
else:
for row in rows:
mac = row[1]
ip = row[2]
start = row[3]
end = row[4] or "..."
rx = row[5] or 0
tx = row[6] or 0
name = row[-2] or row[-1] or mac
text += f"\n<b>{name}</b> ({mac})\nIP: {ip}\nВремя: {start}{end}\nRX: {rx//1024} KB, TX: {tx//1024} KB\n"
# Кнопки: Назад, Обновить, Вперёд (одна строка)
buttons = []
if page > 1:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data=f"history_{period_days}_{page-1}"))
else:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data="noop", disabled=True))
buttons.append(InlineKeyboardButton("Обновить", callback_data=f"history_{period_days}_{page}"))
if page < max_page:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data=f"history_{period_days}_{page+1}"))
else:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data="noop", disabled=True))
kb = InlineKeyboardMarkup(inline_keyboard=[buttons])
if edit and callback:
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=kb)
await callback.answer()
else:
await message.answer(text, parse_mode="HTML", reply_markup=kb)
# --- Команда /clients с постраничностью ---
@dp.message(Command("clients"))
async def clients_cmd(msg: types.Message):
if not await check_user(msg):
return
await send_clients_page(msg, page=1)
@dp.callback_query(lambda c: c.data.startswith("clients_"))
async def clients_page_cb(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS:
await callback.answer("⛔️ Нет доступа", show_alert=True)
return
parts = callback.data.split("_")
page = int(parts[1]) if len(parts) > 1 else 1
await send_clients_page(callback.message, page=page, edit=True, callback=callback)
async def send_clients_page(message, page, edit=False, callback=None):
page_size = 10
rows, total = await get_clients(page=page, page_size=page_size)
max_page = max(1, (total + page_size - 1) // page_size)
text = f"<b>Устройства (стр. {page}/{max_page}):</b>\n"
if not rows:
text += "Нет известных клиентов."
else:
for row in rows:
mac = row[0]
name = row[1] or mac
custom = row[2]
last_seen = row[3]
text += f"\n<b>{custom or name}</b> ({mac})\nПоследняя активность: {last_seen}\n"
# Кнопки: Назад, Обновить, Вперёд (одна строка)
buttons = []
if page > 1:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data=f"clients_{page-1}"))
else:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data="noop", disabled=True))
buttons.append(InlineKeyboardButton("Обновить", callback_data=f"clients_{page}"))
if page < max_page:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data=f"clients_{page+1}"))
else:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data="noop", disabled=True))
kb = InlineKeyboardMarkup(inline_keyboard=[buttons])
if edit and callback:
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=kb)
await callback.answer()
else:
await message.answer(text, parse_mode="HTML", reply_markup=kb)
# --- Обработчик для неактивных кнопок ---
@dp.callback_query(lambda c: c.data == "noop")
async def noop_callback(callback: types.CallbackQuery):
await callback.answer()
# --- Команда /stats с постраничностью ---
@dp.message(Command("stats"))
async def stats_cmd(msg: types.Message):
if not await check_user(msg):
return
await send_stats_page(msg, period_days=30, page=1)
@dp.callback_query(lambda c: c.data.startswith("stats_"))
async def stats_period(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS:
await callback.answer("⛔️ Нет доступа", show_alert=True)
return
parts = callback.data.split("_")
days = int(parts[1])
page = int(parts[2]) if len(parts) > 2 else 1
await send_stats_page(callback.message, period_days=days, page=page, edit=True, callback=callback)
async def send_stats_page(message, period_days, page, edit=False, callback=None):
page_size = 10
rows, total = await get_stats(period_days=period_days, page=page, page_size=page_size)
max_page = max(1, (total + page_size - 1) // page_size)
text = f"<b>Топ по трафику за {period_days} дн. (стр. {page}/{max_page}):</b>\n"
if not rows:
text += "Нет данных за выбранный период."
else:
for row in rows:
mac = row[0]
name = row[1] or row[2] or mac
rx = row[3] or 0
tx = row[4] or 0
count = row[5]
text += f"\n<b>{name}</b> ({mac})\nСессий: {count}\nRX: {rx//1024} KB, TX: {tx//1024} KB, Всего: {(rx+tx)//1024} KB\n"
# Кнопки: Назад, Обновить, Вперёд (одна строка)
buttons = []
if page > 1:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data=f"stats_{period_days}_{page-1}"))
else:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data="noop", disabled=True))
buttons.append(InlineKeyboardButton("Обновить", callback_data=f"stats_{period_days}_{page}"))
if page < max_page:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data=f"stats_{period_days}_{page+1}"))
else:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data="noop", disabled=True))
kb = InlineKeyboardMarkup(inline_keyboard=[buttons])
if edit and callback:
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=kb)
await callback.answer()
else:
await message.answer(text, parse_mode="HTML", reply_markup=kb)
if __name__ == "__main__":
asyncio.run(main())

278
bot_original.py Normal file
View File

@ -0,0 +1,278 @@
import os
import asyncio
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from dotenv import load_dotenv
from librouteros import connect
from librouteros.exceptions import TrapError
from fastapi import FastAPI, Request
import uvicorn
from db import init_db, upsert_client, start_session, update_session, close_session, get_history, get_clients, get_stats
load_dotenv()
TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN")
MT_API_HOST = os.getenv("MT_API_HOST")
MT_API_USER = os.getenv("MT_API_USER")
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)
dp = Dispatcher()
# Авторизация по user_id
async def check_user(message: types.Message) -> bool:
if message.from_user.id not in ALLOWED_USER_IDS:
await message.answer("⛔️ Нет доступа")
return False
return True
# Подключение к MikroTik API
def get_mt_api():
return connect(username=MT_API_USER, password=MT_API_PASS, host=MT_API_HOST)
@dp.message(Command("start"))
async def start_cmd(msg: types.Message):
if not await check_user(msg):
return
kb = InlineKeyboardMarkup()
kb.add(InlineKeyboardButton("Клиенты Wi-Fi", callback_data="clients"))
await msg.answer("Добро пожаловать!", reply_markup=kb)
@dp.callback_query(lambda c: c.data == "clients")
async def show_clients(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS:
await callback.answer("⛔️ Нет доступа", show_alert=True)
return
try:
api = get_mt_api()
clients = api.path('caps-man', 'registration-table').get()
if not clients:
await callback.message.answer("Нет подключённых клиентов Wi-Fi.")
return
text = "<b>Wi-Fi клиенты:</b>\n"
for c in clients:
text += f"\n<b>MAC:</b> {c.get('mac-address')} | <b>IP:</b> {c.get('last-ip', '-')}
<b>VLAN:</b> {c.get('vlan-id', '-')} | <b>AP:</b> {c.get('interface', '-')}
<b>Uptime:</b> {c.get('uptime', '-')}\n"
await callback.message.answer(text, parse_mode="HTML")
except TrapError as e:
await callback.message.answer(f"Ошибка API MikroTik: {e}")
except Exception as e:
await callback.message.answer(f"Ошибка: {e}")
# --- FastAPI для fetch-уведомлений ---
app = FastAPI()
@app.get("/event")
async def event(request: Request):
mac = request.query_params.get('mac')
ip = request.query_params.get('ip')
event_type = request.query_params.get('type')
# Можно добавить секрет для безопасности:
# if request.query_params.get('secret') != os.getenv('MT_SECRET'): return 'forbidden'
text = f"MikroTik: событие {event_type}\nMAC: {mac}\nIP: {ip}"
# Сохраняем клиента и сессию (fetch)
await upsert_client(mac, name=None)
await start_session(mac, ip, source='fetch')
for uid in ALLOWED_USER_IDS:
await bot.send_message(uid, text)
return {"status": "ok"}
# --- Периодический polling MikroTik API для сбора статистики ---
async def poll_mikrotik():
known_clients = {}
while True:
try:
api = get_mt_api()
clients = list(api.path('caps-man', 'registration-table').get())
current_macs = set()
for c in clients:
mac = c.get('mac-address')
ip = c.get('last-ip', '-')
name = c.get('host-name') or c.get('comment')
rx = int(c.get('rx-bytes', 0))
tx = int(c.get('tx-bytes', 0))
await upsert_client(mac, name=name)
await start_session(mac, ip, rx, tx, source='api')
await update_session(mac, rx, tx)
current_macs.add(mac)
# Закрываем сессии для клиентов, которых больше нет
for mac in list(known_clients):
if mac not in current_macs:
await close_session(mac)
known_clients.pop(mac)
known_clients = {mac: True for mac in current_macs}
except Exception as e:
print(f"[poll_mikrotik] Ошибка: {e}")
await asyncio.sleep(30) # интервал опроса
# --- Одновременный запуск бота, FastAPI и polling ---
async def main():
await init_db()
bot_task = asyncio.create_task(dp.start_polling(bot))
config = uvicorn.Config(app, host="0.0.0.0", port=8000, log_level="info")
server = uvicorn.Server(config)
api_task = asyncio.create_task(server.serve())
poll_task = asyncio.create_task(poll_mikrotik())
await asyncio.gather(bot_task, api_task, poll_task)
# --- Команда /history с постраничностью ---
@dp.message(Command("history"))
async def history_cmd(msg: types.Message):
if not await check_user(msg):
return
await send_history_page(msg, period_days=30, page=1)
@dp.callback_query(lambda c: c.data.startswith("history_"))
async def history_period(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS:
await callback.answer("⛔️ Нет доступа", show_alert=True)
return
parts = callback.data.split("_")
days = int(parts[1])
page = int(parts[2]) if len(parts) > 2 else 1
await send_history_page(callback.message, period_days=days, page=page, edit=True, callback=callback)
async def send_history_page(message, period_days, page, edit=False, callback=None):
page_size = 10
rows, total = await get_history(period_days=period_days, page=page, page_size=page_size)
max_page = max(1, (total + page_size - 1) // page_size)
text = f"<b>История подключений за {period_days} дн. (стр. {page}/{max_page}):</b>\n"
if not rows:
text += "Нет подключений за выбранный период."
else:
for row in rows:
mac = row[1]
ip = row[2]
start = row[3]
end = row[4] or "..."
rx = row[5] or 0
tx = row[6] or 0
name = row[-2] or row[-1] or mac
text += f"\n<b>{name}</b> ({mac})\nIP: {ip}\nВремя: {start}{end}\nRX: {rx//1024} KB, TX: {tx//1024} KB\n"
# Кнопки: Назад, Обновить, Вперёд (одна строка)
buttons = []
if page > 1:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data=f"history_{period_days}_{page-1}"))
else:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data="noop", disabled=True))
buttons.append(InlineKeyboardButton("Обновить", callback_data=f"history_{period_days}_{page}"))
if page < max_page:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data=f"history_{period_days}_{page+1}"))
else:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data="noop", disabled=True))
kb = InlineKeyboardMarkup(inline_keyboard=[buttons])
if edit and callback:
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=kb)
await callback.answer()
else:
await message.answer(text, parse_mode="HTML", reply_markup=kb)
# --- Команда /clients с постраничностью ---
@dp.message(Command("clients"))
async def clients_cmd(msg: types.Message):
if not await check_user(msg):
return
await send_clients_page(msg, page=1)
@dp.callback_query(lambda c: c.data.startswith("clients_"))
async def clients_page_cb(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS:
await callback.answer("⛔️ Нет доступа", show_alert=True)
return
parts = callback.data.split("_")
page = int(parts[1]) if len(parts) > 1 else 1
await send_clients_page(callback.message, page=page, edit=True, callback=callback)
async def send_clients_page(message, page, edit=False, callback=None):
page_size = 10
rows, total = await get_clients(page=page, page_size=page_size)
max_page = max(1, (total + page_size - 1) // page_size)
text = f"<b>Устройства (стр. {page}/{max_page}):</b>\n"
if not rows:
text += "Нет известных клиентов."
else:
for row in rows:
mac = row[0]
name = row[1] or mac
custom = row[2]
last_seen = row[3]
text += f"\n<b>{custom or name}</b> ({mac})\nПоследняя активность: {last_seen}\n"
# Кнопки: Назад, Обновить, Вперёд (одна строка)
buttons = []
if page > 1:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data=f"clients_{page-1}"))
else:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data="noop", disabled=True))
buttons.append(InlineKeyboardButton("Обновить", callback_data=f"clients_{page}"))
if page < max_page:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data=f"clients_{page+1}"))
else:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data="noop", disabled=True))
kb = InlineKeyboardMarkup(inline_keyboard=[buttons])
if edit and callback:
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=kb)
await callback.answer()
else:
await message.answer(text, parse_mode="HTML", reply_markup=kb)
# --- Обработчик для неактивных кнопок ---
@dp.callback_query(lambda c: c.data == "noop")
async def noop_callback(callback: types.CallbackQuery):
await callback.answer()
# --- Команда /stats с постраничностью ---
@dp.message(Command("stats"))
async def stats_cmd(msg: types.Message):
if not await check_user(msg):
return
await send_stats_page(msg, period_days=30, page=1)
@dp.callback_query(lambda c: c.data.startswith("stats_"))
async def stats_period(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS:
await callback.answer("⛔️ Нет доступа", show_alert=True)
return
parts = callback.data.split("_")
days = int(parts[1])
page = int(parts[2]) if len(parts) > 2 else 1
await send_stats_page(callback.message, period_days=days, page=page, edit=True, callback=callback)
async def send_stats_page(message, period_days, page, edit=False, callback=None):
page_size = 10
rows, total = await get_stats(period_days=period_days, page=page, page_size=page_size)
max_page = max(1, (total + page_size - 1) // page_size)
text = f"<b>Топ по трафику за {period_days} дн. (стр. {page}/{max_page}):</b>\n"
if not rows:
text += "Нет данных за выбранный период."
else:
for row in rows:
mac = row[0]
name = row[1] or row[2] or mac
rx = row[3] or 0
tx = row[4] or 0
count = row[5]
text += f"\n<b>{name}</b> ({mac})\nСессий: {count}\nRX: {rx//1024} KB, TX: {tx//1024} KB, Всего: {(rx+tx)//1024} KB\n"
# Кнопки: Назад, Обновить, Вперёд (одна строка)
buttons = []
if page > 1:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data=f"stats_{period_days}_{page-1}"))
else:
buttons.append(InlineKeyboardButton("⏪ Назад", callback_data="noop", disabled=True))
buttons.append(InlineKeyboardButton("Обновить", callback_data=f"stats_{period_days}_{page}"))
if page < max_page:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data=f"stats_{period_days}_{page+1}"))
else:
buttons.append(InlineKeyboardButton("Вперёд ⏩", callback_data="noop", disabled=True))
kb = InlineKeyboardMarkup(inline_keyboard=[buttons])
if edit and callback:
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=kb)
await callback.answer()
else:
await message.answer(text, parse_mode="HTML", reply_markup=kb)
if __name__ == "__main__":
asyncio.run(main())

121
db.py Normal file
View File

@ -0,0 +1,121 @@
import aiosqlite
import asyncio
from datetime import datetime
DB_PATH = 'mikrotik_bot.db'
CREATE_CLIENTS = '''
CREATE TABLE IF NOT EXISTS clients (
mac TEXT PRIMARY KEY,
name TEXT,
custom_name TEXT,
last_seen DATETIME
);
'''
CREATE_SESSIONS = '''
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mac TEXT,
ip TEXT,
start_time DATETIME,
end_time DATETIME,
rx_bytes INTEGER,
tx_bytes INTEGER,
source TEXT,
last_update DATETIME,
FOREIGN KEY(mac) REFERENCES clients(mac)
);
'''
async def init_db():
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(CREATE_CLIENTS)
await db.execute(CREATE_SESSIONS)
await db.commit()
async def upsert_client(mac, name=None, custom_name=None):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute('''
INSERT INTO clients (mac, name, custom_name, last_seen)
VALUES (?, ?, ?, ?)
ON CONFLICT(mac) DO UPDATE SET
name=excluded.name,
last_seen=excluded.last_seen
''', (mac, name, custom_name, datetime.now()))
await db.commit()
async def start_session(mac, ip, rx_bytes=0, tx_bytes=0, source='api'):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute('''
INSERT INTO sessions (mac, ip, start_time, rx_bytes, tx_bytes, source, last_update)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (mac, ip, datetime.now(), rx_bytes, tx_bytes, source, datetime.now()))
await db.commit()
async def update_session(mac, rx_bytes, tx_bytes):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute('''
UPDATE sessions SET rx_bytes=?, tx_bytes=?, last_update=?
WHERE mac=? AND end_time IS NULL
''', (rx_bytes, tx_bytes, datetime.now(), mac))
await db.commit()
async def close_session(mac):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute('''
UPDATE sessions SET end_time=?
WHERE mac=? AND end_time IS NULL
''', (datetime.now(), mac))
await db.commit()
async def get_history(period_days=30, page=1, page_size=10):
async with aiosqlite.connect(DB_PATH) as db:
since = datetime.now().timestamp() - period_days*86400
offset = (page - 1) * page_size
async with db.execute('''
SELECT s.*, c.name, c.custom_name FROM sessions s
LEFT JOIN clients c ON s.mac = c.mac
WHERE s.start_time > datetime(?, 'unixepoch')
ORDER BY s.start_time DESC
LIMIT ? OFFSET ?
''', (since, page_size, offset)) as cursor:
rows = await cursor.fetchall()
# Получаем общее количество
async with db.execute('''
SELECT COUNT(*) FROM sessions WHERE start_time > datetime(?, 'unixepoch')
''', (since,)) as cursor:
total = (await cursor.fetchone())[0]
return rows, total
async def get_clients(page=1, page_size=10):
async with aiosqlite.connect(DB_PATH) as db:
offset = (page - 1) * page_size
async with db.execute('''
SELECT * FROM clients ORDER BY last_seen DESC LIMIT ? OFFSET ?
''', (page_size, offset)) as cursor:
rows = await cursor.fetchall()
async with db.execute('SELECT COUNT(*) FROM clients') as cursor:
total = (await cursor.fetchone())[0]
return rows, total
async def get_stats(period_days=30, page=1, page_size=10):
async with aiosqlite.connect(DB_PATH) as db:
since = datetime.now().timestamp() - period_days*86400
offset = (page - 1) * page_size
async with db.execute('''
SELECT s.mac, c.name, c.custom_name, SUM(s.rx_bytes), SUM(s.tx_bytes), COUNT(s.id)
FROM sessions s
LEFT JOIN clients c ON s.mac = c.mac
WHERE s.start_time > datetime(?, 'unixepoch')
GROUP BY s.mac
ORDER BY SUM(s.rx_bytes + s.tx_bytes) DESC
LIMIT ? OFFSET ?
''', (since, page_size, offset)) as cursor:
rows = await cursor.fetchall()
# Получаем общее количество клиентов за период
async with db.execute('''
SELECT COUNT(DISTINCT mac) FROM sessions WHERE start_time > datetime(?, 'unixepoch')
''', (since,)) as cursor:
total = (await cursor.fetchone())[0]
return rows, total

View File

@ -0,0 +1,44 @@
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
# Автообновление контейнеров при изменении кода
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower-mikrotik
restart: unless-stopped
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_POLL_INTERVAL=300 # проверка каждые 5 мин
- WATCHTOWER_SCOPE=mikrotik-bot
- WATCHTOWER_INCLUDE_STOPPED=true
- WATCHTOWER_REVIVE_STOPPED=true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
labels:
- "com.centurylinklabs.watchtower.scope=mikrotik-bot"
networks:
- mikrotik-net
networks:
mikrotik-net:
driver: bridge

View File

@ -0,0 +1,36 @@
version: '3.8'
services:
telegram-bot:
# image: ghcr.io/YOUR_USERNAME/mikrotik-telegram-bot:latest
# Замени на твой registry, пока используем build локально
build:
context: .
dockerfile: Dockerfile
container_name: mikrotik-telegram-bot
env_file:
- .env
restart: unless-stopped
volumes:
- ./data:/app/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
networks:
- mikrotik-net
labels:
# Для Watchtower если используется
- "com.centurylinklabs.watchtower.enable=true"
# Для Portainer webhook
- "io.portainer.accesscontrol.teams.mikrotik-bot"
networks:
mikrotik-net:
driver: bridge
# Примечание: для полного CI/CD нужно:
# 1. Создать .github/workflows/docker.yml для автосборки
# 2. Настроить webhook в Portainer
# 3. Использовать image вместо build

34
docker-compose.yml Normal file
View File

@ -0,0 +1,34 @@
version: '3.8'
services:
telegram-bot:
build: .
container_name: mikrotik-telegram-bot
env_file:
- .env
restart: unless-stopped
volumes:
- ./data:/app/data # Сохранение данных бота (например, SQLite база)
healthcheck:
# Используем curl, который установлен в Dockerfile
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 5s # Даем время на запуск
# Если боту нужно общаться с роутером по IP в локальной сети:
# network_mode: "host"
# Или настройте пользовательскую bridge-сеть Docker, если требуется
# networks:
# - my_local_network
# Пример определения пользовательской сети (если не используется host)
# networks:
# my_local_network:
# driver: bridge
# ipam:
# config:
# - subnet: 172.20.0.0/16 # Пример подсети
# Если нужно пробросить прокси/порт, добавь:
# ports:
# - "8080:8080"

View File

@ -0,0 +1,87 @@
# 🚀 DevOps Infrastructure Deployment Status
## ✅ Deployed Services (Phase 1)
| Service | Container ID | IP | Ports | Status | URL | Notes |
|---------|-------------|-----|-------|--------|-----|-------|
| **Traefik** | Auto | 10.10.30.18 | 8080 | ✅ Running | http://10.10.30.18:8080 | Gateway/Reverse Proxy |
| **Gitea** | 101 | 10.10.30.120 | 3000 | ✅ Running | http://10.10.30.120:3000 | Git + CI/CD + Packages (SQLite) |
| **Docker Registry** | b1e155e920e6 | 10.10.30.121 | 5000 | ✅ Running | http://10.10.30.121:5000 | Private Docker images |
| **Registry UI** | Auto | 10.10.30.121 | 8080 | ✅ Running | http://10.10.30.121:8080 | Registry web interface |
| **Portainer** | 065fd8cfa26b | 10.10.30.121 | 9000 | ✅ Running | http://10.10.30.121:9000 | Docker management |
| **Portainer Agent** | 3a2831b9a481 | 10.10.30.121 | 9001 | ✅ Running | - | Docker agent |
| **Vault** | Auto | 10.10.30.121 | 8200 | ✅ Running | http://10.10.30.121:8200 | Secrets management (token: myroot) |
## 🔧 Working Docker Commands
### Registry with CORS fix:
```bash
docker run -d \
--name registry \
--restart=always \
-p 5000:5000 \
-v /opt/registry-data:/var/lib/registry \
-e REGISTRY_HTTP_HEADERS_ACCESS-CONTROL-ALLOW-ORIGIN='[http://10.10.30.121:8080]' \
-e REGISTRY_HTTP_HEADERS_ACCESS-CONTROL-ALLOW-METHODS='[HEAD,GET,OPTIONS,DELETE]' \
-e REGISTRY_HTTP_HEADERS_ACCESS-CONTROL-ALLOW-CREDENTIALS='[true]' \
-e REGISTRY_HTTP_HEADERS_ACCESS-CONTROL-ALLOW-HEADERS='[Authorization,Accept,Cache-Control]' \
registry:2.8
```
### Registry UI:
```bash
docker run -d \
--name registry-ui \
--restart=always \
-p 8080:80 \
-e REGISTRY_TITLE="DevOps Docker Registry" \
-e REGISTRY_URL="http://10.10.30.121:5000" \
-e REGISTRY_SINGLE="true" \
joxit/docker-registry-ui:latest
```
### Vault (dev mode):
```bash
docker run -d \
--name vault \
--restart=always \
-p 8200:8200 \
-e VAULT_DEV=1 \
-e VAULT_DEV_ROOT_TOKEN_ID=myroot \
hashicorp/vault:latest \
vault server -dev -dev-listen-address="0.0.0.0:8200"
```
## ⏳ Next Phase Services (To Deploy)
| Service | Planned IP | Ports | Purpose |
|---------|-----------|-------|---------|
| **Nexus** | 10.10.30.40 | 8081 | Artifacts, PyPI/npm proxy |
| **Prometheus** | 10.10.30.70 | 9090 | Metrics collection |
| **Grafana** | 10.10.30.80 | 3000 | Monitoring dashboards |
## 🎯 Target Project: MikroTik Telegram Bot
**Source:** `mikrotik_home/telegram_bot/`
**Goal:** Full CI/CD pipeline for auto-deployment
## 📊 Infrastructure Resources
**Proxmox Host:**
- RAM: 62GB (plenty available)
- CPU: 16 cores
- Network: 10.10.30.0/24 (ProxmoxLAN)
- Gateway: 10.10.30.1
**Current Usage:** ~7GB RAM, ~12 CPU cores
## 🔑 Key Learnings
1. **SQLite better than MySQL** for Gitea in LXC
2. **CORS headers required** for Registry UI
3. **Community-scripts work well** for base deployments
4. **Portainer Agent vs Server** distinction important
5. **Registry UI better than Portainer registries** (free)
6. **SonarQube requires PostgreSQL** - complex for MVP
7. **Vault dev mode perfect** for testing
8. **Gitea packages eliminate need** for separate Nexus

View File

@ -0,0 +1,172 @@
# Enterprise DevOps Infrastructure
## Архитектура для 15+ проектов на Proxmox
### 🏗️ Общая схема
```
Internet
[Traefik LB/SSL] ← git.domain.com, registry.domain.com, vault.domain.com
┌─────────────────────────────────────────────────────────────────┐
│ Proxmox Infrastructure │
├─────────────────────────────────────────────────────────────────┤
│ Core Services Network: 10.10.0.0/24 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Traefik │ │ Gitea │ │ Registry │ │
│ │ 10.10.0.10 │ │ 10.10.0.20 │ │ 10.10.0.30 │ │
│ │ (Gateway) │ │ (Git+CI/CD) │ │ (Docker Hub) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Nexus │ │ Vault │ │ SonarQube │ │
│ │ 10.10.0.40 │ │ 10.10.0.50 │ │ 10.10.0.60 │ │
│ │ (Artifacts) │ │ (Secrets) │ │ (Quality) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Prometheus │ │ Grafana │ │
│ │ 10.10.0.70 │ │ 10.10.0.80 │ │
│ │ (Metrics) │ │ (Dashboard) │ │
│ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Production Apps Network: 10.10.1.0/24 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ MikroTik Bot │ │ Project2 │ │ Project3 │ │
│ │ 10.10.1.10 │ │ 10.10.1.20 │ │ 10.10.1.30 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ... до 15 проектов │
└─────────────────────────────────────────────────────────────────┘
```
### 📊 Спецификации LXC контейнеров
| Сервис | IP | RAM | CPU | Disk | Описание |
|--------|-----|-----|-----|------|----------|
| **Traefik** | 10.10.0.10 | 1GB | 2 | 10GB | Reverse proxy, SSL, маршрутизация |
| **Gitea** | 10.10.0.20 | 4GB | 4 | 50GB | Git repos, CI/CD, PostgreSQL |
| **Registry** | 10.10.0.30 | 2GB | 2 | 100GB | Docker images storage |
| **Nexus** | 10.10.0.40 | 6GB | 4 | 200GB | Artifacts, proxy repos |
| **Vault** | 10.10.0.50 | 2GB | 2 | 20GB | Secrets management |
| **SonarQube** | 10.10.0.60 | 4GB | 4 | 30GB | Code quality, PostgreSQL |
| **Prometheus** | 10.10.0.70 | 4GB | 2 | 50GB | Metrics collection |
| **Grafana** | 10.10.0.80 | 2GB | 2 | 20GB | Monitoring dashboards |
| **Apps 1-15** | 10.10.1.x | 1-4GB | 1-2 | 10-30GB | Production applications |
**Итого Core**: ~25GB RAM, 24 CPU, 480GB Disk
**Итого Apps**: ~30GB RAM, 22 CPU, 300GB Disk
**Общий бюджет**: ~55GB RAM, 46 CPU, 780GB Disk
### 🔄 CI/CD Workflow
```mermaid
graph LR
A[git push] --> B[Gitea Actions]
B --> C[SonarQube Scan]
C --> D[Docker Build]
D --> E[Nexus Dependencies]
E --> F[Registry Push]
F --> G[Vault Secrets]
G --> H[Deploy to Prod]
H --> I[Prometheus Metrics]
```
### 🌐 External Access (Traefik Routes)
| Service | URL | Auth |
|---------|-----|------|
| Gitea | `git.yourdomain.com` | Gitea Auth |
| Registry UI | `registry.yourdomain.com` | Basic Auth |
| Nexus | `nexus.yourdomain.com` | Nexus Auth |
| Vault | `vault.yourdomain.com` | Vault Auth |
| SonarQube | `sonar.yourdomain.com` | Sonar Auth |
| Grafana | `monitoring.yourdomain.com` | Grafana Auth |
| Apps | `app1.yourdomain.com` | App-specific |
### 🔧 Технологический стек
**Core Infrastructure:**
- **Traefik 3.0**: HTTP router, SSL automation, load balancing
- **Gitea 1.21**: Git hosting, Actions CI/CD, issue tracking
- **Docker Registry 2.8**: Image storage с UI (registry-ui)
- **Nexus OSS 3.45**: PyPI/npm/Maven proxy, vulnerability scanning
- **Vault 1.15**: Secrets management, dynamic secrets
- **SonarQube CE 10.3**: Code quality, security analysis
**Monitoring Stack:**
- **Prometheus 2.48**: Metrics collection
- **Grafana 10.2**: Visualization, alerting
- **Node Exporter**: System metrics
- **cAdvisor**: Container metrics
**Security & Automation:**
- **Watchtower**: Auto-updates production containers
- **Let's Encrypt**: Automatic SSL certificates
- **Fail2ban**: Intrusion prevention
- **UFW**: Firewall management
### 📁 Project Structure Template
```
project-name/
├── .gitea/
│ └── workflows/
│ ├── build.yml # Build & test
│ ├── quality.yml # SonarQube scan
│ └── deploy.yml # Deploy to production
├── docker/
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── docker-compose.prod.yml
├── infra/
│ ├── vault-secrets.yml # Vault integration
│ ├── monitoring.yml # Prometheus config
│ └── deployment.yml # Production deployment
├── src/ # Application code
├── tests/ # Test suite
├── requirements.txt # Dependencies (via Nexus)
├── sonar-project.properties # SonarQube config
└── README.md
```
### 🚀 Deployment Phases
**Phase 1: Core Infrastructure**
1. Setup Traefik (gateway)
2. Deploy Gitea + PostgreSQL
3. Configure Docker Registry
4. Setup basic monitoring
**Phase 2: DevOps Tools**
1. Deploy Nexus Repository
2. Setup Vault secrets
3. Configure SonarQube
4. Integrate CI/CD pipelines
**Phase 3: Production**
1. Migrate MikroTik bot
2. Setup monitoring alerts
3. Configure auto-deployments
4. Documentation & training
### 💡 Key Benefits
**For Developers:**
- 🔄 Автоматический CI/CD из коробки
- 📦 Быстрые сборки через Nexus cache
- 🛡️ Безопасность через Vault + SonarQube
- 📊 Мониторинг production apps
**For Operations:**
- 🔒 Централизованное управление секретами
- 📈 Полная observability
- 🚀 Zero-downtime deployments
- 💾 Backup & disaster recovery
**For Business:**
- 💰 Экономия на внешних SaaS
- ⚡ Быстрая разработка новых проектов
- 🔐 Контроль над данными
- 📊 Метрики качества кода
Готов начать с автоматизированного развертывания?

191
infra/README.md Normal file
View File

@ -0,0 +1,191 @@
# CI/CD Infrastructure for MikroTik Telegram Bot
Полноценная CI/CD инфраструктура на базе Proxmox для автоматического развертывания MikroTik Telegram Bot.
## 🏗️ Архитектура
```
Developer → Gitea → Actions → Registry → Watchtower → Production
```
### Компоненты:
- **Git Server (10.10.0.10)**: Gitea + PostgreSQL + Actions Runner
- **Registry (10.10.0.20)**: Docker Registry + UI + Watchtower
- **Production (10.10.0.30)**: Portainer + MikroTik Bot + Watchtower
## 🚀 Быстрый старт
### 1. Развертывание инфраструктуры
```bash
cd infra/
chmod +x *.sh
./deploy-all.sh
```
### 2. Первичная настройка
#### Gitea (http://10.10.0.10:3000)
1. Создать админ аккаунт
2. Создать репозиторий `mikrotik-bot`
3. Получить Actions runner token:
- Site Administration → Actions → Runners
- Generate token
4. Обновить токен в контейнере:
```bash
pct exec 100 -- bash -c "cd /opt/gitea && echo 'RUNNER_TOKEN=your_token' > .env"
pct exec 100 -- docker-compose restart runner
```
#### Portainer (http://10.10.0.30:9000)
1. Создать админ аккаунт
2. Настроить webhook для автодеплоя
3. Обновить bot.env с реальными данными
### 3. Настройка проекта
```bash
git clone http://10.10.0.10:3000/your-user/mikrotik-bot.git
cd mikrotik-bot
cp -r .gitea/ ./ # скопировать workflow файлы
git add .
git commit -m "Add CI/CD pipeline"
git push origin main
```
## 📁 Структура файлов
```
infra/
├── setup-lxc.sh # Создание LXC контейнеров
├── deploy-all.sh # Полное развертывание
├── gitea-compose.yml # Git сервер
├── registry-compose.yml # Docker Registry
├── production-compose.yml # Production окружение
└── README.md # Эта инструкция
.gitea/workflows/
└── deploy.yml # CI/CD pipeline
```
## 🔄 Workflow процесс
1. **Push** в main ветку
2. **Gitea Actions** запускает pipeline
3. **Build** Docker образа
4. **Push** в private registry
5. **Watchtower** обнаруживает новый образ
6. **Auto-deploy** в production
## 🛠️ Ручное управление
### Просмотр логов
```bash
# Gitea logs
pct exec 100 -- docker logs gitea
# Registry logs
pct exec 101 -- docker logs docker-registry
# Bot logs
pct exec 102 -- docker logs mikrotik-telegram-bot
```
### Перезапуск сервисов
```bash
# Restart Git server
pct exec 100 -- bash -c "cd /opt/gitea && docker-compose restart"
# Restart Registry
pct exec 101 -- bash -c "cd /opt/registry && docker-compose restart"
# Restart Production
pct exec 102 -- bash -c "cd /opt/production && docker-compose restart"
```
### Обновление конфигурации бота
```bash
pct exec 102 -- nano /opt/production/bot.env
pct exec 102 -- bash -c "cd /opt/production && docker-compose restart mikrotik-bot"
```
## 🔍 Мониторинг
### Проверка статуса
- Git Server: http://10.10.0.10:3000
- Registry UI: http://10.10.0.20:5001
- Portainer: http://10.10.0.30:9000
- Bot Health: http://10.10.0.30:8000/health
### Health checks
```bash
curl http://10.10.0.10:3000/api/v1/version
curl http://10.10.0.20:5000/v2/_catalog
curl http://10.10.0.30:9000/api/status
curl http://10.10.0.30:8000/health
```
## 🔧 Troubleshooting
### Проблемы с registry
```bash
# Проверить конфигурацию insecure registry
pct exec 101 -- cat /etc/docker/daemon.json
pct exec 102 -- cat /etc/docker/daemon.json
# Перезапуск Docker
pct exec 101 -- systemctl restart docker
pct exec 102 -- systemctl restart docker
```
### Проблемы с Actions Runner
```bash
# Проверить статус runner
pct exec 100 -- docker logs gitea-runner
# Перегенерировать токен в Gitea UI
# Обновить .env файл
pct exec 100 -- bash -c "cd /opt/gitea && docker-compose restart runner"
```
### Проблемы с автодеплоем
```bash
# Проверить Watchtower logs
pct exec 102 -- docker logs watchtower-production
# Ручной pull и restart
pct exec 102 -- bash -c "cd /opt/production && docker-compose pull mikrotik-bot && docker-compose up -d mikrotik-bot"
```
## 🔒 Безопасность
- Все сервисы работают в изолированной сети 10.10.0.0/24
- Registry настроен как insecure только для локальной сети
- Рекомендуется настроить VPN для внешнего доступа
- Регулярные снапшоты контейнеров в Proxmox
## 📈 Расширения
### Добавление HTTPS
1. Получить SSL сертификаты
2. Настроить Nginx reverse proxy
3. Обновить конфигурации сервисов
### Мониторинг с Prometheus/Grafana
1. Добавить в registry-compose.yml:
```yaml
prometheus:
image: prom/prometheus
grafana:
image: grafana/grafana
```
### Backup стратегия
1. Автоматические снапшоты LXC
2. Backup Docker volumes
3. Export Gitea repositories
## 🆘 Поддержка
При проблемах:
1. Проверить статус всех контейнеров
2. Просмотреть логи соответствующих сервисов
3. Убедиться в корректности сетевых настроек
4. Проверить доступность портов и DNS записей

187
infra/deploy-all.sh Normal file
View File

@ -0,0 +1,187 @@
#!/bin/bash
# Complete CI/CD Infrastructure Deployment Script
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
echo -e "${BLUE}🚀 MikroTik Bot CI/CD Infrastructure Deployment${NC}"
echo -e "${BLUE}=================================================${NC}"
# Configuration
GIT_SERVER="10.10.0.10"
REGISTRY_SERVER="10.10.0.20"
PRODUCTION_SERVER="10.10.0.30"
# Check if running on Proxmox
check_proxmox() {
if ! command -v pct &> /dev/null; then
echo -e "${RED}❌ This script must be run on Proxmox host${NC}"
exit 1
fi
echo -e "${GREEN}✅ Proxmox detected${NC}"
}
# Deploy containers
deploy_containers() {
echo -e "${YELLOW}📦 Deploying LXC containers...${NC}"
bash setup-lxc.sh
# Wait for containers to be ready
echo -e "${YELLOW}⏳ Waiting for containers to be ready...${NC}"
sleep 30
}
# Deploy Git Server
deploy_git_server() {
echo -e "${YELLOW}🔧 Deploying Git Server (Gitea)...${NC}"
# Copy docker-compose to git server
pct exec 100 -- mkdir -p /opt/gitea
pct push 100 gitea-compose.yml /opt/gitea/docker-compose.yml
# Create .env file
pct exec 100 -- bash -c "cat > /opt/gitea/.env << 'EOF'
RUNNER_TOKEN=placeholder-token-will-be-generated
EOF"
# Deploy Gitea
pct exec 100 -- bash -c "
cd /opt/gitea
docker-compose up -d postgres gitea
echo 'Waiting for Gitea to start...'
sleep 30
"
echo -e "${GREEN}✅ Git Server deployed at http://$GIT_SERVER:3000${NC}"
}
# Deploy Registry
deploy_registry() {
echo -e "${YELLOW}🔧 Deploying Docker Registry...${NC}"
# Copy docker-compose to registry server
pct exec 101 -- mkdir -p /opt/registry
pct push 101 registry-compose.yml /opt/registry/docker-compose.yml
# Configure insecure registry
pct exec 101 -- bash -c "
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'EOF'
{
\"insecure-registries\": [\"$REGISTRY_SERVER:5000\"]
}
EOF
systemctl restart docker
sleep 10
"
# Deploy registry
pct exec 101 -- bash -c "
cd /opt/registry
docker-compose up -d
"
echo -e "${GREEN}✅ Docker Registry deployed at http://$REGISTRY_SERVER:5000${NC}"
echo -e "${GREEN}✅ Registry UI available at http://$REGISTRY_SERVER:5001${NC}"
}
# Deploy Production
deploy_production() {
echo -e "${YELLOW}🔧 Deploying Production Environment...${NC}"
# Configure insecure registry on production
pct exec 102 -- bash -c "
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'EOF'
{
\"insecure-registries\": [\"$REGISTRY_SERVER:5000\"]
}
EOF
systemctl restart docker
sleep 10
"
# Copy docker-compose to production server
pct exec 102 -- mkdir -p /opt/production
pct push 102 production-compose.yml /opt/production/docker-compose.yml
# Create bot.env file
pct exec 102 -- bash -c "cat > /opt/production/bot.env << 'EOF'
TG_BOT_TOKEN=your_bot_token_here
MT_API_HOST=192.168.1.1
MT_API_USER=admin
MT_API_PASS=your_password_here
ALLOWED_USER_IDS=123456789
EOF"
# Deploy production services
pct exec 102 -- bash -c "
cd /opt/production
docker-compose up -d portainer
echo 'Waiting for Portainer to start...'
sleep 20
"
echo -e "${GREEN}✅ Production Environment deployed${NC}"
echo -e "${GREEN}✅ Portainer available at http://$PRODUCTION_SERVER:9000${NC}"
}
# Configure DNS (optional)
configure_dns() {
echo -e "${YELLOW}🌐 DNS Configuration (manual step)${NC}"
echo "Add these entries to your DNS or /etc/hosts:"
echo "$GIT_SERVER git.home.lab"
echo "$REGISTRY_SERVER registry.home.lab"
echo "$PRODUCTION_SERVER portainer.home.lab"
}
# Setup Gitea Actions Runner
setup_runner() {
echo -e "${YELLOW}🏃 Setting up Gitea Actions Runner...${NC}"
echo "Manual steps required:"
echo "1. Go to http://$GIT_SERVER:3000"
echo "2. Create admin account"
echo "3. Go to Site Administration > Actions > Runners"
echo "4. Generate registration token"
echo "5. Update RUNNER_TOKEN in /opt/gitea/.env"
echo "6. Restart gitea runner: docker-compose restart runner"
}
# Main deployment function
main() {
echo -e "${BLUE}Starting deployment...${NC}"
check_proxmox
deploy_containers
deploy_git_server
deploy_registry
deploy_production
configure_dns
setup_runner
echo -e "${GREEN}🎉 Deployment Complete!${NC}"
echo -e "${BLUE}=================================================${NC}"
echo -e "${GREEN}Services:${NC}"
echo -e "📁 Git Server: http://$GIT_SERVER:3000"
echo -e "📦 Registry: http://$REGISTRY_SERVER:5000"
echo -e "🎛️ Registry UI: http://$REGISTRY_SERVER:5001"
echo -e "🔧 Portainer: http://$PRODUCTION_SERVER:9000"
echo ""
echo -e "${YELLOW}Next Steps:${NC}"
echo "1. Configure Gitea admin account"
echo "2. Create mikrotik-bot repository"
echo "3. Setup Gitea Actions runner token"
echo "4. Configure bot.env with real credentials"
echo "5. Push your code to trigger first deployment"
echo ""
echo -e "${BLUE}Happy coding! 🚀${NC}"
}
# Run main function
main "$@"

74
infra/gitea-compose.yml Normal file
View File

@ -0,0 +1,74 @@
version: '3.8'
networks:
gitea:
external: false
volumes:
gitea:
driver: local
postgres:
driver: local
services:
postgres:
image: postgres:14
container_name: gitea-postgres
environment:
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=giteapass
- POSTGRES_DB=gitea
volumes:
- postgres:/var/lib/postgresql/data
networks:
- gitea
restart: unless-stopped
gitea:
image: gitea/gitea:latest
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=postgres:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=giteapass
- GITEA__server__DOMAIN=git.home.lab
- GITEA__server__SSH_DOMAIN=git.home.lab
- GITEA__server__ROOT_URL=http://git.home.lab:3000
- GITEA__server__SSH_PORT=2222
- GITEA__service__DISABLE_REGISTRATION=true
- GITEA__service__REQUIRE_SIGNIN_VIEW=true
- GITEA__actions__ENABLED=true
- GITEA__actions__DEFAULT_ACTIONS_URL=https://github.com
volumes:
- gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
networks:
- gitea
depends_on:
- postgres
restart: unless-stopped
# Gitea Actions Runner
runner:
image: gitea/act_runner:latest
container_name: gitea-runner
environment:
- GITEA_INSTANCE_URL=http://gitea:3000
- GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}
- GITEA_RUNNER_NAME=docker-runner
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./runner-data:/data
networks:
- gitea
depends_on:
- gitea
restart: unless-stopped

View File

@ -0,0 +1,86 @@
version: '3.8'
networks:
production:
external: false
volumes:
portainer_data:
driver: local
bot_data:
driver: local
services:
# Portainer для управления контейнерами
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
command: -H unix:///var/run/docker.sock
environment:
- PORTAINER_HTTP_PORT=9000
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
ports:
- "9000:9000"
networks:
- production
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:9000"]
interval: 30s
timeout: 10s
retries: 3
# MikroTik Telegram Bot
mikrotik-bot:
image: 10.10.0.20:5000/mikrotik-bot:latest
container_name: mikrotik-telegram-bot
env_file:
- bot.env
volumes:
- bot_data:/app/data
networks:
- production
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
labels:
- "com.centurylinklabs.watchtower.enable=true"
- "io.portainer.accesscontrol.teams.mikrotik-bot"
# Watchtower для автообновления бота
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower-production
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_POLL_INTERVAL=60 # каждую минуту для быстрого тестирования
- WATCHTOWER_INCLUDE_STOPPED=true
- WATCHTOWER_REVIVE_STOPPED=true
- WATCHTOWER_LABEL_ENABLE=true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- production
restart: unless-stopped
# Nginx для reverse proxy (опционально)
nginx:
image: nginx:alpine
container_name: nginx-proxy
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
- "443:443"
networks:
- production
depends_on:
- portainer
- mikrotik-bot
restart: unless-stopped

View File

@ -0,0 +1,67 @@
version: '3.8'
networks:
registry:
external: false
volumes:
registry_data:
driver: local
portainer_data:
driver: local
services:
# Private Docker Registry
registry:
image: registry:2
container_name: docker-registry
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
REGISTRY_HTTP_ADDR: 0.0.0.0:5000
REGISTRY_STORAGE_DELETE_ENABLED: "true"
volumes:
- registry_data:/var/lib/registry
ports:
- "5000:5000"
networks:
- registry
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:5000/v2/"]
interval: 30s
timeout: 10s
retries: 3
# Registry UI (optional)
registry-ui:
image: joxit/docker-registry-ui:latest
container_name: registry-ui
environment:
- REGISTRY_TITLE=Private Docker Registry
- REGISTRY_URL=http://registry:5000
- DELETE_IMAGES=true
- SHOW_CONTENT_DIGEST=true
ports:
- "5001:80"
networks:
- registry
depends_on:
- registry
restart: unless-stopped
# Watchtower для автообновления
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower-global
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_POLL_INTERVAL=300
- WATCHTOWER_INCLUDE_STOPPED=true
- WATCHTOWER_REVIVE_STOPPED=true
- WATCHTOWER_NOTIFICATIONS=slack
- WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=${SLACK_WEBHOOK_URL}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- registry
restart: unless-stopped

92
infra/setup-lxc.sh Normal file
View File

@ -0,0 +1,92 @@
#!/bin/bash
# Setup LXC containers for CI/CD infrastructure
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}🚀 Setting up CI/CD Infrastructure on Proxmox${NC}"
# Configuration
TEMPLATE_ID=9000 # Ubuntu 22.04 template ID
STORAGE="local-lvm"
BRIDGE="vmbr1"
GATEWAY="10.10.0.1"
DNS="8.8.8.8"
# Container specifications
declare -A CONTAINERS=(
["git-server"]="100:10.10.0.10:2048:20"
["cicd-registry"]="101:10.10.0.20:4096:50"
["production"]="102:10.10.0.30:2048:20"
)
create_container() {
local name=$1
local config=$2
IFS=':' read -r id ip memory disk <<< "$config"
echo -e "${YELLOW}Creating container: $name (ID: $id)${NC}"
# Create container
pct create $id $TEMPLATE_ID \
--hostname $name \
--cores 2 \
--memory $memory \
--swap 512 \
--storage $STORAGE \
--rootfs $STORAGE:$disk \
--net0 name=eth0,bridge=$BRIDGE,firewall=1,gw=$GATEWAY,ip=$ip/24,type=veth \
--nameserver $DNS \
--features nesting=1,keyctl=1 \
--unprivileged 1 \
--start 1
echo -e "${GREEN}✅ Container $name created and started${NC}"
# Wait for container to boot
sleep 10
# Install Docker
echo -e "${YELLOW}Installing Docker on $name...${NC}"
pct exec $id -- bash -c "
apt update && apt upgrade -y
apt install -y ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo 'deb [arch=\$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \$(lsb_release -cs) stable' | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
systemctl enable docker
systemctl start docker
usermod -aG docker root
"
# Install Docker Compose
pct exec $id -- bash -c "
curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose
"
echo -e "${GREEN}✅ Docker installed on $name${NC}"
}
# Create containers
for container in "${!CONTAINERS[@]}"; do
create_container "$container" "${CONTAINERS[$container]}"
done
echo -e "${GREEN}🎉 All containers created successfully!${NC}"
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Configure DNS: git.home.lab → 10.10.0.10"
echo "2. Deploy services using docker-compose files"
echo "3. Configure webhooks and CI/CD"
echo -e "${GREEN}Container Access:${NC}"
echo "Git Server: ssh root@10.10.0.10 or pct enter 100"
echo "CI/CD: ssh root@10.10.0.20 or pct enter 101"
echo "Production: ssh root@10.10.0.30 or pct enter 102"

97
instructions/README.md Normal file
View File

@ -0,0 +1,97 @@
# Инструкция: Уведомления из MikroTik в Telegram-бот (локальная сеть)
**Внимание!** Все примеры и инструкции ниже рассчитаны на работу внутри одной локальной сети (например, 10.10.10.0/24). Внешний доступ, проброс портов, Cloudflare Tunnel и HTTPS не требуются и не используются.
## Оглавление
- [Вариант 1: Только через API (рекомендуется)](#вариант-1-только-через-api-рекомендуется)
- [Вариант 2: Через fetch-скрипт MikroTik (мгновенные уведомления)](#вариант-2-через-fetch-скрипт-mikrotik-для-мгновенных-событий)
- [Пошагово: fetch-скрипт MikroTik](./mt_fetch_script.md)
- [Пошагово: FastAPI-приёмник событий](./fastapi_receiver.md)
- [Внешний доступ (опционально)](#внешний-доступ-опционально)
---
## Вариант 1: Только через API (рекомендуется)
Бот сам опрашивает MikroTik по внутреннему IP (например, 10.10.10.1) через API и отправляет уведомления в Telegram.
### Преимущества:
- Не нужно ничего настраивать на MikroTik
- Всё централизовано в боте
- Безопасно (нет открытых портов)
### Как работает:
1. В bot.py реализуется периодический опрос событий (например, новых Wi-Fi клиентов, статуса интернета)
2. Если найдено новое событие — бот отправляет уведомление в Telegram
#### Пример кода (добавить в bot.py):
```python
import asyncio
from aiogram import Bot
async def poll_new_clients():
known_macs = set()
while True:
try:
api = get_mt_api()
clients = api.path('caps-man', 'registration-table').get()
new = [c for c in clients if c['mac-address'] not in known_macs]
for c in new:
await bot.send_message(<ВАШ_USER_ID>, f"Новый Wi-Fi клиент: {c['mac-address']} IP: {c.get('last-ip', '-')}")
known_macs.add(c['mac-address'])
except Exception as e:
print('Ошибка опроса MikroTik:', e)
await asyncio.sleep(30) # опрашивать каждые 30 секунд
# В main:
# asyncio.create_task(poll_new_clients())
```
---
## Вариант 2: Через fetch-скрипт MikroTik (для мгновенных событий)
MikroTik сам отправляет HTTP-запрос на сервер с ботом по внутреннему адресу (например, 10.10.10.2:8000).
### Преимущества:
- Мгновенные уведомления (без задержки)
- Не нагружает API частыми запросами
### Недостатки:
- Нужно настраивать скрипт на MikroTik
- Сервер с ботом должен быть доступен по внутреннему адресу
### Как настроить:
1. В MikroTik создайте скрипт:
```
/tool fetch url="http://10.10.10.2:8000/event?type=wifi_connect&mac=$mac&ip=$ip" http-method=get
```
2. Повесьте этот скрипт на событие DHCP lease или Wi-Fi connect (через Scheduler или Lease Script)
3. В боте реализуйте обработку входящих HTTP-запросов (например, через FastAPI или Flask)
4. При получении запроса — отправляйте уведомление в Telegram
#### Пример кода для Flask:
```python
from flask import Flask, request
from aiogram import Bot
app = Flask(__name__)
bot = Bot(token=os.getenv("TG_BOT_TOKEN"))
@app.route('/event')
def event():
mac = request.args.get('mac')
ip = request.args.get('ip')
# ... другие параметры
asyncio.create_task(bot.send_message(<ВАШ_USER_ID>, f"MikroTik: новое подключение {mac} IP: {ip}"))
return 'ok'
```
---
## Какой способ выбрать?
- Если не хотите ничего настраивать на MikroTik — используйте только API (вариант 1)
- Если нужны мгновенные уведомления — используйте fetch-скрипт (вариант 2) или оба варианта вместе
---
## Внешний доступ (опционально)
Если MikroTik и сервер с ботом находятся в разных сетях, используйте Cloudflare Tunnel или проброс портов. Подробнее — см. [Cloudflare Tunnel](./cloudflare_tunnel.md). В обычной локальной схеме это не требуется.

View File

@ -0,0 +1,58 @@
# Cloudflare Tunnel: безопасный доступ к вашему серверу для fetch-уведомлений MikroTik
## 1. Регистрация и подготовка
- Зарегистрируйтесь на https://cloudflare.com (если ещё нет аккаунта)
- Добавьте свой домен в Cloudflare (следуйте инструкциям, смените NS-записи у регистратора)
## 2. Установка cloudflared
- На сервере (где работает бот/FastAPI):
```
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
# или через brew install cloudflared (macOS)
```
## 3. Авторизация и запуск туннеля
- Введите:
```
cloudflared tunnel login
```
- Откроется браузер, авторизуйтесь через Cloudflare.
- Создайте туннель:
```
cloudflared tunnel create mikrotik-bot
cloudflared tunnel route dns mikrotik-bot bot.example.com
```
- Запустите туннель:
```
cloudflared tunnel run mikrotik-bot
```
- Теперь ваш сервер доступен по адресу https://bot.example.com (через Cloudflare, без проброса портов!)
## 4. Настройка fetch-скрипта на MikroTik
- Используйте URL вида:
```
/tool fetch url="https://bot.example.com/event?type=wifi_connect&mac=$mac&ip=$ip" http-method=get
```
## 5. Безопасность
- Cloudflare Tunnel скрывает реальный IP сервера
- Можно ограничить доступ по IP/стране через Cloudflare Firewall
- Рекомендуется добавить секрет в URL (?secret=...) и проверять его на сервере
## 6. Автозапуск (systemd)
- Для автозапуска создайте сервис:
```
sudo cloudflared service install
```
- Проверьте статус:
```
sudo systemctl status cloudflared
```
---
## Преимущества
- Не нужно открывать порты на роутере/сервере
- Бесплатно для большинства задач
- Простая интеграция с MikroTik и Telegram-ботом

View File

@ -0,0 +1,57 @@
# FastAPI: приём fetch-запросов от MikroTik и уведомления в Telegram (локальная сеть)
**Внимание!** Все примеры рассчитаны на работу внутри одной локальной сети. Внешний доступ, HTTPS и туннели не требуются.
## 1. Установка FastAPI и Uvicorn
В папке с ботом:
```
pip install fastapi uvicorn
```
## 2. Пример кода (receiver.py)
```python
import os
import asyncio
from fastapi import FastAPI, Request
from aiogram import Bot
bot = Bot(token=os.getenv("TG_BOT_TOKEN"))
app = FastAPI()
@app.get("/event")
async def event(request: Request):
mac = request.query_params.get('mac')
ip = request.query_params.get('ip')
event_type = request.query_params.get('type')
# Можно добавить секрет для безопасности:
# if request.query_params.get('secret') != os.getenv('MT_SECRET'): return 'forbidden'
text = f"MikroTik: событие {event_type}\nMAC: {mac}\nIP: {ip}"
asyncio.create_task(bot.send_message(<ВАШ_USER_ID>, text))
return {"status": "ok"}
```
- Замените <ВАШ_USER_ID> на свой Telegram user_id (или список).
- Можно вынести user_id в .env и подгружать через os.getenv.
## 3. Запуск FastAPI
```
uvicorn receiver:app --host 0.0.0.0 --port 8000
```
- Если бот и FastAPI в одном контейнере — добавьте запуск FastAPI в Dockerfile/compose.
## 4. Безопасность
- Если сеть полностью изолирована — дополнительных мер не требуется.
- Для production можно добавить секрет в URL и проверку в коде.
## 5. Проверка
- Вручную вызовите fetch-скрипт на MikroTik или curl:
```
curl "http://10.10.10.2:8000/event?type=test&mac=11:22:33:44:55:66&ip=192.168.88.100"
```
- Проверьте, что Telegram-бот прислал уведомление.
---
## TODO
- Пример для запуска FastAPI и aiogram в одном процессе (через asyncio.gather)
- Пример для Docker Compose (несколько сервисов)

View File

@ -0,0 +1,40 @@
# Настройка fetch-скрипта на MikroTik для уведомлений в Telegram-бот (локальная сеть)
**Внимание!** Все примеры рассчитаны на работу внутри одной локальной сети. Внешний доступ, HTTPS и туннели не требуются.
## 1. Создание скрипта на MikroTik
1. Откройте WinBox/WebFig или подключитесь по SSH к вашему MikroTik.
2. Перейдите в меню System → Scripts (или /system script в CLI).
3. Создайте новый скрипт, например:
- Name: notify-telegram
- Source:
```
/tool fetch url="http://10.10.10.2:8000/event?type=wifi_connect&mac=$mac&ip=$ip" http-method=get
```
- Замените 10.10.10.2 на внутренний адрес сервера с ботом.
## 2. Привязка скрипта к событию
### DHCP lease (новое подключение по DHCP)
- В разделе IP → DHCP Server → Leases найдите поле "Lease Script" и добавьте:
```
:if ($leaseBound = true) do={/system script run notify-telegram}
```
- Это вызовет скрипт при каждом новом lease (подключении устройства).
### Wi-Fi connect (CAPsMAN)
- Для CAPsMAN можно использовать Scheduler или Event Handler, если требуется.
## 3. Проверка работы
- Подключите новое устройство к Wi-Fi.
- Проверьте логи MikroTik: появилось ли выполнение скрипта и fetch-запрос.
- Проверьте, что бот получил уведомление (или что сервер принял запрос — можно смотреть логи Flask/FastAPI).
## 4. Безопасность
- Если сеть полностью изолирована — дополнительных мер не требуется.
- Для production можно добавить секрет в URL (например, ?secret=...)
---
## Пример для нескольких событий
- Можно добавить параметры (например, тип события, имя устройства, VLAN и т.д.)
- Скрипт можно вызывать и вручную для теста: `/system script run notify-telegram`

View File

@ -0,0 +1,62 @@
# Системный план: мониторинг MikroTik через Telegram-бота (fetch + API)
## Архитектура и цели
- **Telegram-бот** для мониторинга и управления MikroTik (работает в Docker/Portainer, Proxmox, локальная сеть)
- **Два источника событий:**
- fetch-скрипт MikroTik — для мгновенных алертов о новых подключениях
- API (polling registration-table) — для сбора полной статистики (rx/tx bytes, uptime, отключения)
- **История и статистика** хранятся в SQLite (sessions, clients)
- **Управление и просмотр** через команды и инлайн-кнопки в Telegram
## Ограничения и особенности
- Всё работает внутри локальной сети (нет проброса портов, Cloudflare Tunnel не нужен)
- fetch-скрипт может не содержать rx/tx bytes — только алерт о событии
- API даёт полную картину, но с задержкой (polling)
- Возможны неучтённые данные (VPN, туннели, смена MAC, перезагрузка роутера)
- SQLite выбран как оптимальный вариант для домашней/офисной нагрузки
## Структура данных (БД)
- **clients** (устройства)
- mac (PK)
- name (host-name/comment/vendor)
- custom_name (ручное имя)
- last_seen
- **sessions** (подключения)
- id (PK)
- mac (FK)
- ip
- start_time
- end_time
- rx_bytes
- tx_bytes
- source (fetch/api)
- last_update
## Поведение
- fetch-скрипт: мгновенно пишет событие о новом подключении (если нет активной сессии — создаёт)
- API: периодически обновляет rx/tx bytes, uptime, фиксирует отключения (закрывает сессию)
- Если клиент исчез — сессия закрывается, статистика фиксируется
- Все события и статистика доступны для фильтрации и поиска
## Команды и интерфейс
- [x] /history — история подключений (фильтры: день, неделя, месяц, постраничность)
- [x] /clients — список устройств (сортировка по last_seen, постраничность)
- [x] /stats — агрегированная статистика (топ по трафику, активности, постраничность)
- [x] Инлайн-кнопки для фильтрации, навигации, обновления
- [x] fetch+API: гибридная обработка событий, антидублирование, запись в SQLite
- [x] Асинхронная работа, интеграция с aiogram и FastAPI
- [x] Вся логика реализована для работы в локальной сети
- [ ] Возможность задать кастомное имя устройству через бота
- [ ] (Опционально) Экспорт истории/статистики в CSV/Excel
- [ ] (Опционально) Алерты по подозрительной активности (например, новый MAC, много трафика)
---
**Всё основное реализовано!**
**Оставшиеся задачи:**
- Возможность задавать кастомные имена устройствам через бота
- (Опционально) Экспорт истории/статистики
- (Опционально) Алерты по событиям
**Этот документ — эталонный план для всей разработки и поддержки проекта.**

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
aiogram>=3.0.0
librouteros>=3.0.0
python-dotenv>=1.0.0
fastapi>=0.100.0
uvicorn>=0.22.0
aiosqlite>=0.19.0