Add MikroTik bot source code and Dockerfile
This commit is contained in:
parent
428d5afacb
commit
521bb90611
83
AI_CONTEXT.md
Normal file
83
AI_CONTEXT.md
Normal 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
58
DEPLOY.md
Normal 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
12
Dockerfile
Normal 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
240
INFRA_PLAN.md
Normal 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
|
||||||
35
README.md
35
README.md
@ -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
317
bot.py
Normal 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
278
bot_original.py
Normal 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
121
db.py
Normal 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
|
||||||
44
docker-compose.portainer.yml
Normal file
44
docker-compose.portainer.yml
Normal 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
|
||||||
36
docker-compose.registry.yml
Normal file
36
docker-compose.registry.yml
Normal 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
34
docker-compose.yml
Normal 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"
|
||||||
87
infra/DEPLOYMENT_STATUS.md
Normal file
87
infra/DEPLOYMENT_STATUS.md
Normal 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
|
||||||
172
infra/ENTERPRISE_ARCHITECTURE.md
Normal file
172
infra/ENTERPRISE_ARCHITECTURE.md
Normal 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
191
infra/README.md
Normal 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
187
infra/deploy-all.sh
Normal 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
74
infra/gitea-compose.yml
Normal 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
|
||||||
86
infra/production-compose.yml
Normal file
86
infra/production-compose.yml
Normal 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
|
||||||
67
infra/registry-compose.yml
Normal file
67
infra/registry-compose.yml
Normal 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
92
infra/setup-lxc.sh
Normal 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
97
instructions/README.md
Normal 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). В обычной локальной схеме это не требуется.
|
||||||
58
instructions/cloudflare_tunnel.md
Normal file
58
instructions/cloudflare_tunnel.md
Normal 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-ботом
|
||||||
57
instructions/fastapi_receiver.md
Normal file
57
instructions/fastapi_receiver.md
Normal 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 (несколько сервисов)
|
||||||
40
instructions/mt_fetch_script.md
Normal file
40
instructions/mt_fetch_script.md
Normal 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`
|
||||||
62
instructions/plan_system.md
Normal file
62
instructions/plan_system.md
Normal 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
6
requirements.txt
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user