Telegram-бот как системный администратор сервера
При запуске сервера часто необходимо предоставить доступ к части функционала другим пользователям, при этом сами пользователи могут не иметь достаточных компетенций для полноценного использования софта и/или мы хотим ограничить доступный набор команд.
Одним из вариантов решения является Telegram-бот, который является прослойкой между пользователем и софтом. С таким решением я встречался на реальном опыте уже как минимум два раза, и на основе одного из них я постараюсь объяснить, как это можно сделать.
Дисклеймер
По специальности я программист C++ для Unreal Engine, а девопс и администрирование сервера — это часть пет-проекта и интереса к области, поэтому если вы знаете другие методы или способы по улучшению приведённого ниже, буду рад пообщаться в комментариях и/или лично.
Системный администратор мечты. Работает за киловатты в час
Введение
Как я уже сказал у меня в опыте есть два примера.
Первый пример — это коммерческий проект, в котором я не успел поучаствовать, но знаю архитектурную часть. Одной из основных функций являлась возможность добавления контента в приложение художниками уже после релиза без вмешательства разработчиков. Для этого художники отправляли контент в специальном формате телеграм боту, и он им собирал новую версию приложения.
Второй пример уже из серии «сделай сам для себя». Недавно я запустил свой домашний сервер на Ubuntu, о котором вкратце рассказал в публикации. На этом сервере я запустил сервер Minecraft, на котором играл с друзьями. Чтобы серверный компьютер не был постоянно нагружен игровым сервером, мне нужно было дать возможность друзьям безболезненно включать и выключать сервер без моего участия, если они захотели часок другой поиграть без меня. Соответственно я решил воспользоваться возможностью и изучил как это можно сделать с помощью телеграм бота.
Вот на основе создания второго бота и будем смотреть, как написать и запустить подобное решение.
Telegram-бот
На просторах интернета полно стартовых гайдов по созданию телеграм ботов на Python, поэтому про основу бота буду писать кратко. Сам я нашёл много информации начиная с этой статьи и онлайн книги по библиотеке aiogram.
Настройка окружения
Первое, что мы делаем это создаёт директорию где будут лежать наши бот (ы), в котором создадим виртуальное окружение и установим aiogram и python-dotenv для файлов конфигурации.
mkdir telegram-bots
cd telegram-bots/
# Создание виртуального окружения
python3 -m venv bots-venv
# Входим в виртуальное окружение
. bots-venv/bin/activate
# Установка библиотек
pip install aiogram python-dotenv pydantic pydantic_settings
# Выходим из виртуального окружения
deactivate
Для бота я создал отдельную поддиректорию
mkdir minecraft-bot
cd minecraft-bot
Основа бота
Для начала напишем основу бота, которая будет отвечать на команду /start
и выводить клавиатуру для управления.
Создадим три файла minecraft_bot.py c основным кодом бота, config_reader.py для чтения .env файла и сам .env для хранения токена бота, который получается от BotFather.
minecraft_bot.py:
minecraft_bot.py Основа
# импорты
import asyncio
import logging
from aiogram import Bot, Dispatcher, types, F
from aiogram.filters.command import Command
from config_reader import config
# Включаем логирование, чтобы не пропустить важные сообщения
logging.basicConfig(level=logging.INFO)
# Бот с токеном из конфига
bot = Bot(token=config.bot_token.get_secret_value())
# Диспетчер, получающий апдейты телеграмма
dp = Dispatcher()
# Хэндлер на команду /start
@dp.message(Command("start"))
async def cmd_start(message: types.Message):
# Клавиатура с тремя кнопками
kb = [
[types.KeyboardButton(text="Check server status")],
[types.KeyboardButton(text="Start server")],
[types.KeyboardButton(text="Stop server")],
]
keyboard = types.ReplyKeyboardMarkup(keyboard=kb)
await message.answer("Hi! \nThis bot can tell you server status and help you to start and stop it", reply_markup=keyboard)
# Хэндлеры кнопок
@dp.message(F.text.lower() == "check server status")
async def check_server_status(message: types.Message):
await message.answer("Can't Check Server Status")
@dp.message(F.text.lower() == "start server")
async def check_server_status(message: types.Message):
await message.answer("Starting server")
@dp.message(F.text.lower() == "stop server")
async def check_server_status(message: types.Message):
await message.answer("Stopping server")
# Запуск процесса поллинга новых апдейтов
async def main():
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
config_reader.py:
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import SecretStr
class Settings(BaseSettings):
# Желательно вместо str использовать SecretStr
# для конфиденциальных данных, например, токена бота
bot_token: SecretStr
# Начиная со второй версии pydantic, настройки класса настроек задаются
# через model_config
# В данном случае будет использоваться файла .env, который будет прочитан
# с кодировкой UTF-8
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')
# При импорте файла сразу создастся
# и провалидируется объект конфига,
# который можно далее импортировать из разных мест
config = Settings()
.env:
BOT_TOKEN = 12345678:AaBbCcDdEeFfGgHh
Первое общение с ботом
Для запуска выполняем команду python3 minecraft_bot.py
в виртуальном окружении. В результате у нас есть ещё не самый умный, но уже рабочий бот, с удобными кнопками и ответами на их нажатие.
Bash скрипты
Итак, напомню, что мы создаём бота для управления сервером, то есть нам нужно уметь запускать процессы и отслеживать работают они или нет. Для этого нужно научиться запускать bash команды из программы Python (воспользуемся билиотеками os и subprocess), а также написать bash скрипты для остановки и запуска сервера Minecraft. Все bash скрипты создаём в той же директори, что и код для бота, и не забываем прописывать им разрешение на запуск sudo chmod +x *.sh
.
Начнём с bash скриптов start-server.sh:
#!/bin/bash
# Переходим в директорию сервера
cd /home/user/NewServer
# Запускаем сервер в бекграунде, перенаправляем его вывод в nohup.output и отвязываем связь с скриптом
nohup java @user_jvm_args.txt @libraries/net/minecraftforge/forge/1.18.2-40.2.9/unix_args.txt nogui > nohup.output 2>&1 &
# Выводим pid последнего запущенного процесса (сервера) в консоль и в файл server.pid
echo $!
echo $! > /home/user/telegram-bots/minecraft-bot/server.pid
И stop-server.sh:
#!/bin/bash
# Проверяем что существует процесс с pid переданном в первом аргументе и убиваем его
if ps -p $1 > /dev/null
then
echo "Trying to stop pid $1"
kill $1
else
echo "No pid"
fi
Для проверки работы процесса используем библиотеку os:
import os
def check_pid(pid):
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
Для запуска bash скриптов — subprocess:
# Запуск скрипта старта сервера и запись вывода в переменную result
result = subprocess.run(["sh", "./start-server.sh"], capture_output=True, text=True).stdout
# Запуск скрипта остановки сервера (процесса с pid)
subprocess.call(["sh", "./stop-server.sh", pid])
Хранение данных
Последний момент, которого нам не хватает для запуска бота — это хранение pid’а запущенного процесса. Pid — это сокращение от process id, то есть уникальный номер процесса который сейчас исполняется в системе. Для хранения pid можно было бы использовать глобальную переменную, но я противник не константных глобальных переменных. Поэтому запишем всё в файл json, с помощью одноименной библиотеки (при большем количестве данных можно подключать базу данных):
Использование json для хранения pid
import json
import os.path
# Имя json-файла и переменной в нём
SERVER_DATA_PATH = 'minecraft_server.json'
PID = "pid"
# Получение всех данных из json-файла
def get_server_data():
if os.path.isfile(SERVER_DATA_PATH):
with open(SERVER_DATA_PATH, 'r') as openfile:
return json.load(openfile)
else:
return {}
# Запись всех данных в json-файла
def set_server_data(server_data):
with open(SERVER_DATA_PATH, 'w') as f:
json.dump(server_data, f)
# Запись всех данных в json-файла с изменённым значением
def set_server_data_value(key, value):
server_data = get_server_data()
server_data[key] = value
set_server_data(server_data)
def check_pid(pid):
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
# Проверка запущен ли сервер
def check_server_process():
server_data = get_server_data()
if not PID in server_data:
return False
if server_data[PID] == -1:
return False
return check_pid(server_data[PID])
Вот полный код полученного бота:
minecraft_bot.py
# импорты
import asyncio
import logging
from aiogram import Bot, Dispatcher, types, F
from aiogram.filters.command import Command
from config_reader import config
import subprocess
import json
import os.path
import os
# Имя json-файла и переменной в нём
SERVER_DATA_PATH = 'minecraft_server.json'
PID = "pid"
# Получение всех данных из json-файла
def get_server_data():
if os.path.isfile(SERVER_DATA_PATH):
with open(SERVER_DATA_PATH, 'r') as openfile:
return json.load(openfile)
else:
return {}
# Запись всех данных в json-файла
def set_server_data(server_data):
with open(SERVER_DATA_PATH, 'w') as f:
json.dump(server_data, f)
# Запись всех данных в json-файла с изменённым значением
def set_server_data_value(key, value):
server_data = get_server_data()
server_data[key] = value
set_server_data(server_data)
def check_pid(pid):
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
# Проверка запущен ли сервер
def check_server_process():
server_data = get_server_data()
if not PID in server_data:
return False
if server_data[PID] == -1:
return False
return check_pid(server_data[PID])
# Включаем логирование, чтобы не пропустить важные сообщения
logging.basicConfig(level=logging.INFO)
# Бот с токеном из конфига
bot = Bot(token=config.bot_token.get_secret_value())
# Диспетчер, получающий апдейты телеграмма
dp = Dispatcher()
# Хэндлер на команду /start
@dp.message(Command("start"))
async def cmd_start(message: types.Message):
# Клавиатура с тремя кнопками
kb = [
[types.KeyboardButton(text="Check server status")],
[types.KeyboardButton(text="Start server")],
[types.KeyboardButton(text="Stop server")],
]
keyboard = types.ReplyKeyboardMarkup(keyboard=kb)
await message.answer("Hi! \nThis bot can tell you server status and help you to start and stop it", reply_markup=keyboard)
# Хэндлеры кнопок
@dp.message(F.text.lower() == "check server status")
async def check_server_status(message: types.Message):
if check_server_process():
await message.answer("Server is running")
return
await message.answer("Server is stopped")
@dp.message(F.text.lower() == "start server")
async def check_server_status(message: types.Message):
if check_server_process():
await message.answer("Server is already running")
return
result = subprocess.run(["sh", "./start-server.sh"], capture_output=True, text=True).stdout
set_server_data_value(PID, int(result))
await message.answer("Starting server")
@dp.message(F.text.lower() == "stop server")
async def check_server_status(message: types.Message):
server_data = get_server_data()
if check_server_process():
subprocess.call(["sh", "./stop-server.sh", str(server_data[PID])])
set_server_data_value(PID, -1)
await message.answer("Stoping server")
return
await message.answer("Server is already stopped")
# Запуск процесса поллинга новых апдейтов
async def main():
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Автозапуск бота
Последний штрих этого проекта — это автозапуск нашего бота при включении Ubuntu сервера и чистка процессов при неожиданных вылетах бота. Для этого снова обратимся к bash скриптам и напишем сервис, который будет их запускать.
start.sh для запуска телеграм бота, все команды уже были рассмотрены ранее:
#!/bin/bash
cd /home/user/telegram-bots
. bots-venv/bin/activate
cd minecraft-bot
python3 minecraft_bot.py
stop.sh для остановки сервера в случае падения бота (вот тут нам и пригодился pid записанный в файл server.pid)
#!/bin/bash
cd /home/user/telegram-bots/minecraft-bot
# Читаем pid Minecraft сервера
server_pid=$(head -n 1 server.pid)
# Останавливаем Minecraft сервер и удаляем файлы с pid данными
sh ./stop-server.sh "$server_pid"
rm minecraft_server.json
rm server.pid
Теперь осталось только написать сервис, который будет запускаться с помощью systemd, создадим файл /etc/systemd/system/minecraft-bot.service:
[Unit]
Description=Minecraft Server Telegram Bot
[Service]
ExecStart=/home/user/telegram-bots/minecraft-bot/start.sh
ExecStop=/home/user/telegram-bots/minecraft-bot/stop.sh
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
Здесь мы указали какие скрипты запускать при запуске (ExecStart
) и остановке (ExecStop
) сервиса. Также прописали, что сервис необходимо перезапустить при его падении (Restart=on-failure
) и пробовать это сделать каждые 5 секунд (RestartSec=5s
).
После чего выполняем следующие команды для запуска сервиса:
sudo systemctl daemon-reload
sudo systemctl enable minecraft-bot.service
sudo systemctl start minecraft-bot.service
Вот на этом моменте создание бота можно считать оконченным.
Заключение
В этой статье я хотел отразить весь процесс создания Telegram-бота для администрирования сервера Ubuntu, а именно на примере запуска, остановки и проверки работы сервера Minecraft.
Плюсы данного решения заключаются в том, что:
Дружелюбный интерфейс. Пользователю не надо быть гением linux и программирования, чтобы взаимодействовать с софтом на сервере.
Лёгкий доступ. Не надо иметь доступ по ip, так как все запросы идут через сервер Telegram. Кроме этого бот позволяет работать с любого устройство с установленным Telegram
Гибкий контроль доступа. Разработчик полностью контролирует функционал доступный пользователю, а также можно ввести базу данных с авторизацией для разных уровней доступа.
Из минусов могу выделить:
Сложность решения. Много трудозатрат может уйти на само создание бота, особенно с более сложно логикой, что может быть менее выгодно, чем обучить пользователя работать с функционалом без красивой прослойки.
P.S.
Материал был собран на основе самостоятельного обучения, поэтому всегда буду рад предложениям по улучшению в комментариях или личном общении.