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.

Материал был собран на основе самостоятельного обучения, поэтому всегда буду рад предложениям по улучшению в комментариях или личном общении.


ссылка на оригинал статьи https://habr.com/ru/articles/796365/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *