mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-04-29 01:00:03 +00:00
Revert "Add automated installer and monitoring script"
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,8 +8,6 @@
|
||||
!app-config.json
|
||||
!main.py
|
||||
!requirements.txt
|
||||
!scripts/
|
||||
!scripts/**
|
||||
!docs/
|
||||
!docs/**
|
||||
|
||||
|
||||
17
README.md
17
README.md
@@ -91,24 +91,9 @@ sudo chown -R 1000:1000 ./logs ./data
|
||||
docker compose up -d
|
||||
|
||||
# 5. Проверь статус
|
||||
docker compose logs
|
||||
docker compose logs
|
||||
```
|
||||
|
||||
### 🧰 Автоустановщик и мониторинг
|
||||
|
||||
Для автоматической настройки сервера теперь доступен скрипт `scripts/install_monitor.py`.
|
||||
Он скачает (или обновит) репозиторий, подготовит директории и права, соберёт базовые
|
||||
значения `.env`, настроит обратный прокси (Caddy/Nginx или отдельный контейнер Caddy),
|
||||
запустит Docker-сервисы и откроет интерактивный мониторинг с кнопками перезапуска.
|
||||
|
||||
```bash
|
||||
python3 scripts/install_monitor.py
|
||||
```
|
||||
|
||||
Во время выполнения скрипт спросит домены для вебхуков, мини-приложения и страницы
|
||||
редиректа, а также значения `BOT_TOKEN`, `ADMIN_IDS`, `REMNAWAVE_API_URL`,
|
||||
`REMNAWAVE_API_KEY` и параметры авторизации панели.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Конфигурация
|
||||
|
||||
@@ -1,639 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Automated installer and monitoring utility for the Remnawave Bedolaga bot."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
|
||||
REPO_URL = "https://github.com/fr1ngg/remnawave-bedolaga-telegram-bot.git"
|
||||
DEFAULT_INSTALL_DIR = Path("/opt/remnawave-bot")
|
||||
DEFAULT_CADDY_DIR = Path("/opt/caddy")
|
||||
BOT_SERVICE_NAME = "remnawave_bot"
|
||||
REQUIRED_DIRECTORIES = [
|
||||
Path("logs"),
|
||||
Path("data"),
|
||||
Path("data/backups"),
|
||||
Path("data/referral_qr"),
|
||||
]
|
||||
DIR_PERMISSIONS = "755"
|
||||
DIR_OWNER = "1000:1000"
|
||||
ENV_FIELDS = [
|
||||
("BOT_TOKEN", True, "Введите токен Telegram-бота"),
|
||||
("ADMIN_IDS", True, "ID администраторов через запятую"),
|
||||
(
|
||||
"REMNAWAVE_API_URL",
|
||||
True,
|
||||
"Укажите URL панели (например, https://panel.example.com или http://remnawave:3000)",
|
||||
),
|
||||
("REMNAWAVE_API_KEY", True, "Ключ доступа к панели"),
|
||||
]
|
||||
OPTIONAL_PROMPTS = [
|
||||
(
|
||||
"REMNAWAVE_AUTH_TYPE",
|
||||
["api_key", "basic_auth"],
|
||||
"Тип авторизации панели (api_key/basic_auth)",
|
||||
),
|
||||
("REMNAWAVE_USERNAME", None, "Имя пользователя для Basic Auth (опционально)"),
|
||||
("REMNAWAVE_PASSWORD", None, "Пароль для Basic Auth (опционально)"),
|
||||
(
|
||||
"REMNAWAVE_SECRET_KEY",
|
||||
None,
|
||||
"Секретный ключ панели (формат XXXXXXX:DDDDDDDD для установок eGames)",
|
||||
),
|
||||
]
|
||||
CADDY_MARKER_START = "# === REMNAWAVE BOT AUTOGENERATED START ==="
|
||||
CADDY_MARKER_END = "# === REMNAWAVE BOT AUTOGENERATED END ==="
|
||||
NGINX_MARKER_START = " # === REMNAWAVE BOT AUTOGENERATED START ==="
|
||||
NGINX_MARKER_END = " # === REMNAWAVE BOT AUTOGENERATED END ==="
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProxyConfig:
|
||||
webhook_domain: Optional[str]
|
||||
miniapp_domain: Optional[str]
|
||||
redirect_domain: Optional[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProxySetupResult:
|
||||
embedded_created: bool
|
||||
restart_containers: List[str]
|
||||
|
||||
|
||||
class CommandError(RuntimeError):
|
||||
"""Raised when a shell command fails."""
|
||||
|
||||
|
||||
def run_command(
|
||||
command: Iterable[str],
|
||||
*,
|
||||
cwd: Optional[Path] = None,
|
||||
check: bool = True,
|
||||
capture_output: bool = False,
|
||||
text: bool = True,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Execute a command and display it for transparency."""
|
||||
|
||||
cmd_list = list(command)
|
||||
print(f"\n$ {' '.join(shlex.quote(part) for part in cmd_list)}")
|
||||
result = subprocess.run(
|
||||
cmd_list,
|
||||
cwd=str(cwd) if cwd else None,
|
||||
check=False,
|
||||
capture_output=capture_output,
|
||||
text=text,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
raise CommandError(
|
||||
f"Команда {' '.join(cmd_list)} завершилась с кодом {result.returncode}:\n"
|
||||
f"STDOUT: {result.stdout}\nSTDERR: {result.stderr}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def ensure_repo(target_dir: Path) -> Path:
|
||||
"""Clone the repository or update an existing checkout."""
|
||||
|
||||
if target_dir.exists():
|
||||
print(f"Каталог {target_dir} уже существует. Попытаемся обновить репозиторий.")
|
||||
if (target_dir / ".git").exists():
|
||||
run_command(["git", "pull"], cwd=target_dir)
|
||||
else:
|
||||
print("Каталог существует, но не является git-репозиторием. Пропускаем обновление.")
|
||||
else:
|
||||
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
run_command(["git", "clone", REPO_URL, str(target_dir)])
|
||||
return target_dir
|
||||
|
||||
|
||||
def ensure_directories(base_dir: Path) -> None:
|
||||
"""Create required directories and ensure permissions."""
|
||||
|
||||
for rel_path in REQUIRED_DIRECTORIES:
|
||||
full_path = base_dir / rel_path
|
||||
full_path.mkdir(parents=True, exist_ok=True)
|
||||
print("Настраиваем права доступа и владельцев для каталогов данных...")
|
||||
run_command(["chmod", "-R", DIR_PERMISSIONS, "./logs", "./data"], cwd=base_dir)
|
||||
chown_cmd = ["chown", "-R", DIR_OWNER, "./logs", "./data"]
|
||||
if os.geteuid() != 0:
|
||||
chown_cmd.insert(0, "sudo")
|
||||
try:
|
||||
run_command(chown_cmd, cwd=base_dir)
|
||||
except CommandError as exc:
|
||||
print(f"Не удалось изменить владельца: {exc}. Продолжим.")
|
||||
|
||||
|
||||
def prompt(
|
||||
text: str,
|
||||
*,
|
||||
default: Optional[str] = None,
|
||||
required: bool = False,
|
||||
validator: Optional[Iterable[str]] = None,
|
||||
) -> Optional[str]:
|
||||
"""Prompt user for input with optional validation."""
|
||||
|
||||
allowed = set(validator) if validator else None
|
||||
while True:
|
||||
suffix = f" [{default}]" if default else ""
|
||||
value = input(f"{text}{suffix}: ").strip()
|
||||
if not value and default is not None:
|
||||
value = default
|
||||
if not value and required:
|
||||
print("Это обязательное значение. Повторите ввод.")
|
||||
continue
|
||||
if allowed and value and value not in allowed:
|
||||
print(f"Недопустимое значение. Возможные варианты: {', '.join(sorted(allowed))}")
|
||||
continue
|
||||
return value or None
|
||||
|
||||
|
||||
def load_env(path: Path) -> Dict[str, str]:
|
||||
env: Dict[str, str] = {}
|
||||
if path.exists():
|
||||
for line in path.read_text().splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
if "=" in stripped:
|
||||
key, value = stripped.split("=", 1)
|
||||
env[key.strip()] = value
|
||||
return env
|
||||
|
||||
|
||||
def save_env(path: Path, updates: Dict[str, Optional[str]]) -> None:
|
||||
"""Update the .env file preserving comments and unknown keys."""
|
||||
|
||||
lines: List[str] = []
|
||||
existing_env = load_env(path)
|
||||
updated_keys: set[str] = set()
|
||||
|
||||
if path.exists():
|
||||
source_lines = path.read_text().splitlines()
|
||||
else:
|
||||
example_path = path.with_suffix(".example")
|
||||
source_lines = example_path.read_text().splitlines() if example_path.exists() else []
|
||||
|
||||
for line in source_lines:
|
||||
stripped = line.strip()
|
||||
if stripped and not stripped.startswith("#") and "=" in line:
|
||||
key, _ = line.split("=", 1)
|
||||
key = key.strip()
|
||||
if key in updates:
|
||||
value = updates[key]
|
||||
if value is None:
|
||||
value = existing_env.get(key, "")
|
||||
lines.append(f"{key}={value}")
|
||||
updated_keys.add(key)
|
||||
continue
|
||||
lines.append(line)
|
||||
|
||||
for key, value in updates.items():
|
||||
if key not in updated_keys:
|
||||
lines.append(f"{key}={value or ''}")
|
||||
|
||||
path.write_text("\n".join(lines) + "\n")
|
||||
print(f"Файл {path} обновлён.")
|
||||
|
||||
|
||||
def configure_env(base_dir: Path) -> None:
|
||||
env_path = base_dir / ".env"
|
||||
env_values = load_env(env_path)
|
||||
print("\n=== Настройка файла .env ===")
|
||||
|
||||
updates: Dict[str, Optional[str]] = {}
|
||||
for key, required, description in ENV_FIELDS:
|
||||
current = env_values.get(key)
|
||||
updates[key] = prompt(description, default=current, required=required)
|
||||
|
||||
auth_type_default = env_values.get("REMNAWAVE_AUTH_TYPE")
|
||||
auth_type = prompt(
|
||||
"Нужен тип авторизации панели? (оставьте пустым, чтобы пропустить)",
|
||||
default=auth_type_default,
|
||||
validator=["api_key", "basic_auth", ""],
|
||||
)
|
||||
if auth_type:
|
||||
updates["REMNAWAVE_AUTH_TYPE"] = auth_type
|
||||
|
||||
if auth_type == "basic_auth":
|
||||
for key, _, description in OPTIONAL_PROMPTS[1:3]:
|
||||
current = env_values.get(key)
|
||||
updates[key] = prompt(description, default=current)
|
||||
else:
|
||||
updates["REMNAWAVE_USERNAME"] = env_values.get("REMNAWAVE_USERNAME")
|
||||
updates["REMNAWAVE_PASSWORD"] = env_values.get("REMNAWAVE_PASSWORD")
|
||||
|
||||
secret_current = env_values.get("REMNAWAVE_SECRET_KEY")
|
||||
updates["REMNAWAVE_SECRET_KEY"] = prompt(
|
||||
OPTIONAL_PROMPTS[-1][2], default=secret_current
|
||||
)
|
||||
|
||||
save_env(env_path, updates)
|
||||
|
||||
|
||||
def ensure_docker_compose() -> List[str]:
|
||||
for candidate in (["docker", "compose"], ["docker-compose"]):
|
||||
try:
|
||||
run_command(candidate + ["version"], capture_output=True)
|
||||
return candidate
|
||||
except CommandError:
|
||||
continue
|
||||
raise RuntimeError("Docker Compose не найден. Установите docker-compose или docker compose.")
|
||||
|
||||
|
||||
def run_compose(base_dir: Path, command: List[str]) -> None:
|
||||
compose = ensure_docker_compose()
|
||||
run_command(compose + command, cwd=base_dir)
|
||||
|
||||
|
||||
def detect_proxy_container(image_keywords: Iterable[str]) -> Optional[tuple[str, Dict]]:
|
||||
result = run_command(
|
||||
["docker", "ps", "--format", "{{.ID}} {{.Image}} {{.Names}}"],
|
||||
capture_output=True,
|
||||
)
|
||||
lines = (result.stdout or "").splitlines()
|
||||
for line in lines:
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
container_id, image, name = parts[0], parts[1], parts[2]
|
||||
combined = f"{image} {name}".lower()
|
||||
if any(keyword in combined for keyword in image_keywords):
|
||||
inspect = run_command(["docker", "inspect", container_id], capture_output=True)
|
||||
data = json.loads(inspect.stdout)[0]
|
||||
return name, data
|
||||
return None
|
||||
|
||||
|
||||
def find_config_mount(data: Dict, target_filename: str) -> Optional[Path]:
|
||||
mounts = data.get("Mounts", [])
|
||||
for mount in mounts:
|
||||
destination = mount.get("Destination", "")
|
||||
if destination.endswith(target_filename) or destination.endswith(os.path.basename(target_filename)):
|
||||
source = mount.get("Source")
|
||||
if source:
|
||||
return Path(source)
|
||||
for mount in mounts:
|
||||
destination = mount.get("Destination", "")
|
||||
if destination.endswith(os.path.dirname(target_filename).rstrip("/")):
|
||||
source = mount.get("Source")
|
||||
if source:
|
||||
return Path(source) / os.path.basename(target_filename)
|
||||
return None
|
||||
|
||||
|
||||
def render_caddy_snippet(proxy: ProxyConfig) -> str:
|
||||
lines = [CADDY_MARKER_START]
|
||||
if proxy.webhook_domain:
|
||||
lines.extend(
|
||||
[
|
||||
f"{proxy.webhook_domain} {{",
|
||||
" encode gzip",
|
||||
" @yookassa path /yookassa*",
|
||||
" reverse_proxy @yookassa remnawave_bot:8082",
|
||||
" @pal24 path /pal24-webhook*",
|
||||
" reverse_proxy @pal24 remnawave_bot:8084",
|
||||
" reverse_proxy remnawave_bot:8081",
|
||||
"}",
|
||||
]
|
||||
)
|
||||
if proxy.miniapp_domain:
|
||||
lines.extend(
|
||||
[
|
||||
f"{proxy.miniapp_domain} {{",
|
||||
" encode gzip",
|
||||
" reverse_proxy remnawave_bot:8080",
|
||||
"}",
|
||||
]
|
||||
)
|
||||
if proxy.redirect_domain:
|
||||
lines.extend(
|
||||
[
|
||||
f"{proxy.redirect_domain} {{",
|
||||
" encode gzip",
|
||||
" handle_path /* {",
|
||||
" reverse_proxy remnawave_bot:8080",
|
||||
" }",
|
||||
"}",
|
||||
]
|
||||
)
|
||||
lines.append(CADDY_MARKER_END)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_nginx_snippet(proxy: ProxyConfig) -> str:
|
||||
snippets: List[str] = []
|
||||
if proxy.webhook_domain:
|
||||
snippets.append(
|
||||
textwrap.dedent(
|
||||
f"""
|
||||
server {{
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
server_name {proxy.webhook_domain};
|
||||
|
||||
location /yookassa {{
|
||||
proxy_pass http://remnawave_bot:8082;
|
||||
include /etc/nginx/proxy_params;
|
||||
}}
|
||||
|
||||
location /pal24-webhook {{
|
||||
proxy_pass http://remnawave_bot:8084;
|
||||
include /etc/nginx/proxy_params;
|
||||
}}
|
||||
|
||||
location / {{
|
||||
proxy_pass http://remnawave_bot:8081;
|
||||
include /etc/nginx/proxy_params;
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
).strip()
|
||||
)
|
||||
if proxy.miniapp_domain:
|
||||
snippets.append(
|
||||
textwrap.dedent(
|
||||
f"""
|
||||
server {{
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
server_name {proxy.miniapp_domain};
|
||||
|
||||
location / {{
|
||||
proxy_pass http://remnawave_bot:8080;
|
||||
include /etc/nginx/proxy_params;
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
).strip()
|
||||
)
|
||||
if proxy.redirect_domain:
|
||||
snippets.append(
|
||||
textwrap.dedent(
|
||||
f"""
|
||||
server {{
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
server_name {proxy.redirect_domain};
|
||||
|
||||
location / {{
|
||||
proxy_pass http://remnawave_bot:8080;
|
||||
include /etc/nginx/proxy_params;
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
).strip()
|
||||
)
|
||||
if not snippets:
|
||||
return ""
|
||||
block = "\n\n".join(snippets)
|
||||
indented = "\n".join(f" {line}" if line else line for line in block.splitlines())
|
||||
return "\n".join([NGINX_MARKER_START, indented, NGINX_MARKER_END, ""])
|
||||
|
||||
|
||||
def update_caddy_config(config_path: Path, snippet: str) -> bool:
|
||||
if not snippet.strip():
|
||||
return False
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = config_path.read_text() if config_path.exists() else ""
|
||||
if CADDY_MARKER_START in content and CADDY_MARKER_END in content:
|
||||
start = content.index(CADDY_MARKER_START)
|
||||
end = content.index(CADDY_MARKER_END) + len(CADDY_MARKER_END)
|
||||
content = content[:start].rstrip() + "\n\n" + content[end:].lstrip()
|
||||
new_content = (content.rstrip() + "\n\n" + snippet).strip() + "\n"
|
||||
if config_path.exists() and config_path.read_text() == new_content:
|
||||
return False
|
||||
config_path.write_text(new_content)
|
||||
print(f"Конфигурация Caddy обновлена: {config_path}")
|
||||
return True
|
||||
|
||||
|
||||
def update_nginx_config(config_path: Path, snippet: str) -> bool:
|
||||
if not snippet.strip():
|
||||
return False
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = config_path.read_text() if config_path.exists() else ""
|
||||
if NGINX_MARKER_START in content and NGINX_MARKER_END in content:
|
||||
start = content.index(NGINX_MARKER_START)
|
||||
end = content.index(NGINX_MARKER_END) + len(NGINX_MARKER_END)
|
||||
content = content[:start].rstrip() + "\n" + content[end:].lstrip()
|
||||
new_content = (content.rstrip() + "\n\n" + snippet).strip() + "\n"
|
||||
if config_path.exists() and config_path.read_text() == new_content:
|
||||
return False
|
||||
config_path.write_text(new_content)
|
||||
print(f"Конфигурация Nginx обновлена: {config_path}")
|
||||
return True
|
||||
|
||||
|
||||
def ensure_network(name: str) -> None:
|
||||
result = run_command(["docker", "network", "ls", "--format", "{{.Name}}"], capture_output=True)
|
||||
networks = set(filter(None, (result.stdout or "").splitlines()))
|
||||
if name not in networks:
|
||||
run_command(["docker", "network", "create", name])
|
||||
else:
|
||||
print(f"Сеть {name} уже существует.")
|
||||
|
||||
|
||||
def container_exists(name: str) -> bool:
|
||||
result = run_command(["docker", "ps", "-a", "--format", "{{.Names}}"], capture_output=True)
|
||||
containers = set(filter(None, (result.stdout or "").splitlines()))
|
||||
return name in containers
|
||||
|
||||
|
||||
def connect_container_to_network(container: str, network: str) -> None:
|
||||
if not container_exists(container):
|
||||
print(f"Контейнер {container} не найден. Пропускаем подключение к сети {network}.")
|
||||
return
|
||||
result = run_command(["docker", "network", "inspect", network], capture_output=True)
|
||||
data = json.loads(result.stdout)
|
||||
containers = data[0].get("Containers", {})
|
||||
if container not in containers:
|
||||
run_command(["docker", "network", "connect", network, container])
|
||||
else:
|
||||
print(f"Контейнер {container} уже находится в сети {network}.")
|
||||
|
||||
|
||||
def ensure_bot_network(base_dir: Path) -> str:
|
||||
result = run_command(["docker", "network", "ls", "--format", "{{.Name}}"], capture_output=True)
|
||||
networks = (result.stdout or "").splitlines()
|
||||
project_name = base_dir.name.replace(" ", "")
|
||||
expected = f"{project_name}_bot_network"
|
||||
for network in networks:
|
||||
if network.endswith("bot_network"):
|
||||
expected = network
|
||||
break
|
||||
ensure_network(expected)
|
||||
return expected
|
||||
|
||||
|
||||
def prepare_embedded_caddy(proxy: ProxyConfig) -> bool:
|
||||
DEFAULT_CADDY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
compose_path = DEFAULT_CADDY_DIR / "docker-compose.yml"
|
||||
caddyfile_path = DEFAULT_CADDY_DIR / "Caddyfile"
|
||||
snippet = render_caddy_snippet(proxy)
|
||||
changed = update_caddy_config(caddyfile_path, snippet)
|
||||
compose_content = textwrap.dedent(
|
||||
"""
|
||||
version: '3.8'
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
container_name: remnawave_caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
networks:
|
||||
- remnawave_shared
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
networks:
|
||||
remnawave_shared:
|
||||
external: true
|
||||
"""
|
||||
).strip() + "\n"
|
||||
compose_path.write_text(compose_content)
|
||||
ensure_network("remnawave_shared")
|
||||
print(f"Caddy будет запущен с конфигурацией в {DEFAULT_CADDY_DIR}")
|
||||
return changed
|
||||
|
||||
|
||||
def start_embedded_caddy() -> None:
|
||||
compose = ensure_docker_compose()
|
||||
run_command(compose + ["up", "-d"], cwd=DEFAULT_CADDY_DIR)
|
||||
|
||||
|
||||
def setup_proxy(proxy: ProxyConfig) -> ProxySetupResult:
|
||||
caddy_container = detect_proxy_container(["caddy"])
|
||||
nginx_container = detect_proxy_container(["nginx"])
|
||||
containers_to_restart: List[str] = []
|
||||
|
||||
if caddy_container:
|
||||
name, data = caddy_container
|
||||
print(f"Найден контейнер Caddy: {name}")
|
||||
config_path = find_config_mount(data, "Caddyfile")
|
||||
if config_path:
|
||||
if update_caddy_config(config_path, render_caddy_snippet(proxy)):
|
||||
containers_to_restart.append(name)
|
||||
else:
|
||||
print("Не удалось определить путь Caddyfile. Конфигурация не изменена.")
|
||||
return ProxySetupResult(False, containers_to_restart)
|
||||
if nginx_container:
|
||||
name, data = nginx_container
|
||||
print(f"Найден контейнер Nginx: {name}")
|
||||
config_path = find_config_mount(data, "nginx.conf")
|
||||
if config_path:
|
||||
if update_nginx_config(config_path, render_nginx_snippet(proxy)):
|
||||
containers_to_restart.append(name)
|
||||
else:
|
||||
print("Не удалось определить путь nginx.conf. Конфигурация не изменена.")
|
||||
return ProxySetupResult(False, containers_to_restart)
|
||||
print("Caddy/Nginx в Docker не обнаружены. Будет создан отдельный инстанс Caddy.")
|
||||
changed = prepare_embedded_caddy(proxy)
|
||||
return ProxySetupResult(changed, containers_to_restart)
|
||||
|
||||
|
||||
def start_services(base_dir: Path) -> None:
|
||||
print("\n=== Запуск контейнеров бота ===")
|
||||
run_compose(base_dir, ["pull"])
|
||||
run_compose(base_dir, ["up", "-d", "--remove-orphans"])
|
||||
|
||||
network = ensure_bot_network(base_dir)
|
||||
ensure_network("remnawave_shared")
|
||||
connect_container_to_network(BOT_SERVICE_NAME, "remnawave_shared")
|
||||
connect_container_to_network("remnawave_caddy", network)
|
||||
|
||||
|
||||
def restart_container(name: str) -> None:
|
||||
if not container_exists(name):
|
||||
print(f"Контейнер {name} не найден, перезапуск невозможен.")
|
||||
return
|
||||
run_command(["docker", "restart", name])
|
||||
|
||||
|
||||
def compose_logs(base_dir: Path, service: str, tail: str = "100") -> None:
|
||||
run_compose(base_dir, ["logs", service, "--tail", tail])
|
||||
|
||||
|
||||
def compose_status(base_dir: Path) -> None:
|
||||
print("\n=== Состояние контейнеров ===")
|
||||
run_compose(base_dir, ["ps"])
|
||||
|
||||
|
||||
def monitoring_loop(base_dir: Path) -> None:
|
||||
while True:
|
||||
compose_status(base_dir)
|
||||
print(
|
||||
"\nДоступные действия:\n"
|
||||
" [R] Перезапустить бот\n"
|
||||
" [L] Показать последние логи\n"
|
||||
" [U] Обновить репозиторий и перезапустить\n"
|
||||
" [Q] Выход"
|
||||
)
|
||||
choice = input("Ваш выбор: ").strip().lower()
|
||||
if choice == "r":
|
||||
restart_container(BOT_SERVICE_NAME)
|
||||
elif choice == "l":
|
||||
compose_logs(base_dir, BOT_SERVICE_NAME)
|
||||
elif choice == "u":
|
||||
run_command(["git", "pull"], cwd=base_dir)
|
||||
start_services(base_dir)
|
||||
elif choice == "q":
|
||||
print("Выход из мониторинга.")
|
||||
break
|
||||
else:
|
||||
print("Неизвестная команда.")
|
||||
|
||||
|
||||
def request_proxy_domains() -> ProxyConfig:
|
||||
print("\n=== Настройка доменов для прокси ===")
|
||||
webhook = prompt("Домен для вебхуков (например, hooks.example.com)")
|
||||
miniapp = prompt("Домен мини-приложения (например, miniapp.example.com)")
|
||||
redirect = prompt("Домен страницы редиректа (например, redirect.example.com)")
|
||||
return ProxyConfig(webhook, miniapp, redirect)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
install_dir_input = prompt(
|
||||
"Каталог установки бота", default=str(DEFAULT_INSTALL_DIR), required=True
|
||||
)
|
||||
if not install_dir_input:
|
||||
print("Не указан каталог установки.")
|
||||
return 1
|
||||
install_dir = Path(install_dir_input).expanduser()
|
||||
|
||||
ensure_repo(install_dir)
|
||||
ensure_directories(install_dir)
|
||||
configure_env(install_dir)
|
||||
|
||||
proxy_config = request_proxy_domains()
|
||||
proxy_result = setup_proxy(proxy_config)
|
||||
if proxy_result.embedded_created:
|
||||
start_embedded_caddy()
|
||||
for container in proxy_result.restart_containers:
|
||||
restart_container(container)
|
||||
|
||||
start_services(install_dir)
|
||||
monitoring_loop(install_dir)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\nОтменено пользователем.")
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user