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