Меня зовут Вячеслав, и я руководитель отдела маркетинга. Я поднял VPN-туннель по подписке на базе ispmanager. Ispmanager поддерживает WireGuard, который разворачивается за пару кликов. Минимум головной боли. Однако мне этого было мало: нужно было, чтобы по окончании подписки туннель автоматически отключался и статистика по каждому пользователю собиралась ежедневно. Панель по умолчанию предоставляет только суммарное количество использованного трафика по каждому пользователю, что тоже неплохо, но для моих целей не подходит.

У меня нет опыта самостоятельной разработки, кодинга и работы с консолью. Поэтому всё, что я дальше буду описывать, — это мои мытарства в попытке получить нужный результат. Все работы проводятся с помощью Python.

Задачи преследовал две:

  1. Если пользователь перестал потреблять ресурсы, значит, он скоро отвалится или уже отвалился, и нужно с ним работать.

  2. Если у пользователя закончилась подписка, она должна автоматически отключаться.

Содержание

  1. Немного матчасти ispmanager

    1. Управление изнутри сервера – утилита mgrctl

    2. Управление снаружи сервера – API-запросы

  2. Логика работы скрипта

  3. Рекомендации по сбору скрипта

  4. Этап 0. Настройка Google Sheets

  5. Этап 1. Подключение Google Sheets и библиотек Python

  6. Этап 2. Получение пользователей WireGuard из ispmanager

  7. Этап 3. Передача статистики в Google Sheets

  8. Этап 4. Автоматическое отключение пользователей WireGuard через mgrctl

  9. Этап 5. Загрузка и запуск сприпта на Python в ispmanager

    1. Установка Python

    2. Создание виртуального окружения

    3. Установка библиотек и запуск

  10. Резюме

Немного матчасти ispmanager

Управление изнутри сервера — утилита mgrctl

Управлять без интерфейса (работать с API) в ispmanager можно несколькими путями: изнутри сервера и снаружи. В ситуации, когда скрипт расположен на сервере, разработчики рекомендуют использовать утилиту mgrctl, которая управляет панелью. При этом запрос к утилите всегда выглядит одинаково:

/usr/local/mgr5/sbin/mgrctl -m ispmgr [функция] [команда],

где -m ispmgr означает, что обращаемся мы именно к ispmanager, а не к фреймворку coremanager, на котором написана панель.

Например, если мы хотим получить информацию обо всех возможностях mgrctl, надо в shell закинуть команду /usr/local/mgr5/sbin/mgrctl -m ispmgr -i. В ответ получим довольно длинный список возможных функций, но без описания параметров.

Скриншот некоторых функций mgrctl
Скриншот некоторых функций mgrctl

Управление снаружи сервера — API-запросы

Второй вариант работы с ispmanager без интерфейса — это API-запросы. В официальной документации есть огромная статья с описанием большинства возможных функций. В соседней статье мы узнаем, что для любого взаимодействия по API нужна авторизация. Собственно, как и везде. Авторизоваться можно несколькими способами:

  1. С использованием уникального номера сессии — подходит, когда нужно много работать с API. В первый раз получаем номер сессии и в последующем отправляем не пароль, а номер.

  2. Через Authinfo — нужен для разового взаимодействия, когда в одном запросе передаются и логин, и пароль.

  3. Сквозная авторизация по ключу — нужна для подключения из других систем. Как правило, чтобы автоматически авторизовать клиента в ispmanager. Для нашей ситуации совсем не подходит.

В документации деталей больше. В этой статье мы будем использовать Authinfo.

Среди доступных методов у нас GET и POST, а также возможность задавать формат вывода для результата запроса.

Логика работы скрипта

Перед тем как начать ковыряться с Python и API, я решил прописать логику работы скрипта.

Повторим задачи:

  1. Собирать ежедневную статистику потребления трафика по каждому пользователю.

  2. Отключать пользователей, если подписка закончилась.

Чтобы это провернуть, нам нужно:

  1. Куда-то складывать статистику.

  2. Где-то хранить данные о сроках действия подписок и о пользователях.

Сначала я было ткнулся в платные решения, но на текущем уровне развития проекта получалось шибко дорого. Поэтому что? Правильно — любимые Google Sheets, они подходят под обе задачи.

В ispmanager пользователи VPN хранятся в виде таблицы, значит, для наших задач нужно было сравнивать список пользователей ispmanager и список пользователей в Google Sheets. В итоге получилась следующая схема:

Схема работы скрипта
Схема работы скрипта

Небольшое отступление: сервис со временем развился, и появился второй протокол, помимо WireGuard. Из-за него гипотетически возможна ситуация, когда пользователь будет в таблице, но его не будет в ispmanager.

Рекомендации по сбору скрипта

Строчек кода получается не очень много, но в любом случае, рекомендую обкладывать всё дебагом. Перед запуском вы всегда успеете его почистить, но подробный дебаг даст вам сильно больше информации о том, что вообще происходит в скрипте в тот или иной момент.

Кроме того, поскольку в скрипте я использую и API-запросы, которые можно отправлять с локальной машины, и вызов функции mgrctl, которую нужно делать изнутри сервера, целесообразно вначале разбить скрипт на несколько файлов и тестировать их отдельно друг от друга. Когда всё будет готово, просто соберёте один файл на сервере.

Если у вас возникнут сложности с ispmanager, не стесняйтесь писать ребятам в техподдержку. Они отвечают быстро и по делу. Писать скрипт за вас они бесплатно не будут. При необходимости, есть платные профсервисы, которые помогают с системным администрированием и настройкой сервера. Но вот разобраться с панелью или помочь запустить скрипт смогут легко и бесплатно.

Этап 0. Настройка Google Sheets

Для начала подготовим Google Sheets. Логика скрипта предполагает размещение статистики на ежедневной основе. Поэтому нам понадобится таблица, где в первом столбце перечислены пользователи, а в первой строчке дни месяца.

Чтобы статистика была нагляднее, мы можем сделать автособираемую таблицу. Для этого:

  1. Заполните все столбцы и строки любыми цифрами, позже там появится статистика потреблённого трафика. 

  2. Выделите получившийся массив.

  3. Нажмите Вставка -> Диаграмма.

У вас автоматически соберётся что-то подобное:

График не обязательно будет линейным, но соберётся автоматически
График не обязательно будет линейным, но соберётся автоматически

Далее вы убираете все добавленные ранее цифры и спокойно ждёте. Когда скрипт отработает, графики будут строиться автоматически.

Пример графика на более правдоподобных данных
Пример графика на более правдоподобных данных

График потом можно будет унести стандартными средствами на другую страницу, чтобы он не мешал в таблице.

Этап 1. Подключение Google Sheets и библиотек

На Хабре довольно много статей, как с помощью Python попасть в Google Sheets. Сейчас я кратко обрисую, как и что делать. Если вам потребуется ещё информация, рекомендую статьи, по которым учился сам:

Во-первых, вам понадобится библиотека gspread. У неё нормально описанная документация, в случае необходимости можно обращаться к ней. Устанавливается через pip3 install gspread.

Во-вторых, вам понадобится файл credits.json, именно он будет обеспечивать подключения к таблицам. 

Для этого:

  1. Переходим в Google Cloud Console.

  2. Нажимаем Create project и создаём проект.

  3. Проваливаемся в настройки через троеточие в конце строки и переходим в раздел сервисных аккаунтов.

  4. Создаём сервисный аккаунт, во втором пункте ставим роль «Владелец».

  5. После создания двойным кликом проваливаемся внутрь аккаунта и переходим во вкладку «ключи». Добавляем новый ключ в формате json. На компьютер скачался файл, это и есть нужный нам credits.json.

  6. Вверху страницы в поле поиска вбиваем sheets, выбираем Google Sheets API, проваливаемся внутрь и жмём кнопку «Подключить». Аналогично делаем для Google Drive.

  7. Теперь копируем почту сервисного аккаунта и даём ей редакторский доступ в нужную нам таблицу.

  8. У таблицы есть spreadsheetId, который имеет вид 1DuFovkpm-QXKHYHFHHEP3fojkkiN8sU9ngHTWhaY555. Его берём прямо из URL таблицы, это значение между /d/ и /edit.

С этим разобрались. Теперь, когда нам потребуется подключить гугл-таблицу в скрипт, мы можем сделать это командами:

Import gspread
gs = gspread.service_account(filename='путь_до_файла/credits.json')
sh = gs.open_by_key('spreadsheetId')

Этап 2. Получение пользователей WireGuard из ispmanager

WireGuard был новой фичей, и развёрнутую документацию по API подвезти ещё не успели. Но! Как показала практика, в вопросе API ispmanager можно было обойтись и без документации. В одной из статей документации говорится, что можно закинуть в shell команду tail -f /usr/local/mgr5/var/ispmgr.log | grep Request, потом выполнить в другом окне браузера в панели нужное действие и увидеть, что происходит под капотом. А в логе вы увидите что-то вроде того, что на скриншоте ниже.

Пример вывода лога по команде
Пример вывода лога по команде

Поскольку мы работаем с WireGuard, я сходил в соседнюю вкладку, выключил, а потом включил пользователя. В логе мы увидим получение таблицы с пользователями, выключение и включение пользователя.

Теперь из выделенной записи соберём части запроса: нам нужно всё, что после имени пользователя, удаляем лишнюю информацию вроде out=xjson&tconvert=undefined и двигаемся в сторону API.

Добавим к уже готовому коду из предыдущего пункта обработку XML. Это нужно, чтобы обрабатывать ответы, полученные по API от ispmgr. Для работы с XML я использовал модуль etree.

Import xml.etree.ElementTree as ET

Для понимания дальнейшей логики работы с XML рекомендую немного почитать, как строятся файлы в этом формате. Если коротко, все XML-файлы имеют древовидную структуру, похожую на структуру блоков в HTML, вроде:

<lvl1>text
	<lvl2>text</lvl2>
</lvl1>

Добавим библиотеку для работы с API-запросами

Import requests

Формируем запрос согласно документации и полученной от панели информации. Мне нужно обратиться один раз, поэтому я буду использовать authinfo.

Res = requests.request(‘get’, ‘https://IP:1500/ispmgr?authinfo=admin:password&func=wireguard.user&xml=out’)

Мы получили список пользователей ispmgr. Если вы дальше поставите команду

print(res.text)

увидите, что вам прислала панель. Если не ставить приставку .text, то вы увидите только код ответа, но не увидите его содержание.

Этап 3. Передача статистики в Google Sheets

Дальше нам необходимо обработать эту информацию. А именно: понять, сколько трафика использовал каждый пользователь, и записать эту информацию в Google Sheets.

Чтобы записывать в нужную ячейку, необходимо на уровне скрипта автоматически узнавать, какая сейчас дата. Для этого подключим модуль работы с датами и заведём дату в переменную.

From datetime import datetime
Current_date = datetime.now()

Далее подключимся к нужной нам странице в таблице и получим столбец с именами пользователей. Для ускорения работы скрипта уберём из списка заголовок, для работы он нам не нужен.

Worksheet = sh.worksheet(“Sheet_name”)
User_names = worksheet.col_values(номер столбца)
User_names.remove(‘Заголовок’)

Настало время обработать всё, что мы насобирали. Строим XML-дерево из полученного строкового списка, который хранится у нас в переменной res.

Root = ET.fromstringlist(res)

Теперь начинаем искать нужные нам данные. Каждый пользователь будет заведён через верхний уровень elem, поэтому перебираем циклом все elem, которые лежат на верхнем уровне дерева.

For elem in root.findall(‘elem’):

Далее нужно найти объём переданного трафика для найденного elem:

Sentsize = elem.find(‘sentsize’).text

Результат этого действия мы получим в формате Х.XX GB, что не подойдёт для построения графика: помешает размерность и точка в значении. Поэтому кусок с размерностью нужно удалить из строки, а точку заменить запятой.

Sentsize = setnsize.strip(‘ GB’)
Sentsize = sensize.replace(“.”, “,”)

После этого нам нужно узнать, что это за пользователь вообще.

Name = elem.find(‘name’).text

И теперь через цикл внутри цикла нужно перебрать все значения имён пользователей из гугл-таблицы, чтобы найти нужное и подставить туда полученные значения. Дополнительно мы введём три переменные i = индекс текущего цикла, a = координата a и b = координата b. Первая строчка у нас — даты месяца, а первый столбик — список имён. Значит, от них нужно будет отступить одну клетку.

i = 0
For word in user_name:
	A = i+1
	B = current_date.day+1
	If name = user_name[i]:
		Worksheet = sh.worksheet(“Sheet_name”)
		Worksheet = worksheet.update_cell(a, b, sensize);
	i += 1

Возникает логичный вопрос, зачем каждый раз обращаться к странице в таблице, если мы её уже подключили, когда брали пользователей. Но иначе при втором проходе цикл попытается вставить старые координаты, и вы получите ошибку о том, что координат больше, чем должно быть. Я пока не очень понял, почему получается именно так, но без повторного обращения к листу таблицы вылезает ошибка. Если есть идеи, буду рад услышать их в комментариях.

Этап 4. Отключение пользователей WG через mgrctl

Одну задачу мы выполнили, осталась вторая — отключать юзеров, когда у них закончилась подписка. 

Сразу оговорюсь, можно отключать пользователей через API и через mgrctl. Я погружался в API впервые, поэтому использовал оба метода. Но можно остановиться и на одном, более подходящем под ваши задачи.

Mgrctl — утилита, и запустить её напрямую из Python не выйдет, потому что нет библиотек. Но вы можете использовать подпроцесс. Для начала нужно подключиться к той таблице и листу, где у вас лежат пользователи и срок действия их лицензий. Я не ограничиваю пользователей по трафику, поэтому нужно следить только за сроком. Плюс я использую ту же таблицу, поэтому просто подключаюсь к другому листу.

Worksheet = sh.worksheet(“worksheet_name”)

Если порядок пользователей отличается от порядка на этапе статистики, необходимо снова получить их список и убрать заголовок.

User_name = worksheet.col_values(номер столбца)
User_name.remove(‘Заголовок’)

Ещё нам нужно получить даты действия лицензий.

Licence = worksheet.col_values(номер столбца)
Licence.remove(‘заголовок’)

Далее формируем цикл, в котором сравниваем даты действия лицензии из таблиц и текущую дату. Предварительно импортируем модуль подпроцессов, поскольку он понадобится внутри цикла, а также заводим индекс выполнения цикла.

i = 0
Import subprocess
For word in user_name:

Дата, которую мы получили из гугл-таблиц, представляет собой текстовую строку. Когда мы заходим сравнить отдельные её части через .moth или .day, сделать это не получится. Поэтому нужно изменить строку в готовый для сравнения вид.

Date_for_comparison = licence[i]
Date_for_comparison = datetime.strptime(date_for_comparison, “%d.%m.%Y”)

Дальше происходит непосредственно сравнение. Для нас важны две ситуации:

  1. Порядковое число месяца окончания лицензии больше, чем у текущего месяца. Если этого не проверять, рискуем отключить лицензии, продлённые на срок больше месяца.

  2. Порядковое число дня окончания лицензии больше, чем у текущего дня.

Кроме этого, важно помнить, что для отключения пользователя нам нужно обращаться к mgrctl, а значит, нужно строкой передать данные. Но в подпроцесс у меня не получилось добавить переменную, только захардкоженную строку, хотя вроде как технически это возможно. А без переменной мы не сможем передать, какого конкретно пользователя отключить, без перебора всех вариантов.

Хорошая новость в том, что подпроцесс умеет использовать переменную для запуска подпроцесса. А значит, мы можем в переменную добавить другую переменную, которая учтётся при её формировании. Осталось только монитор присобачить… 

Построим условия для ситуаций и обработку отключений пользователей.

If date_for_comparison.month >= current_date.moth:
	print(‘month success’)
Else:
	If date_for_comparison.day >= current_date.day:
		print(‘day success’)
	Else:
		print(‘day fail’)
		Elid = ‘elid=’+user_name[i]
		Url = '/usr/local/mgr5/sbin/mgrctl'+' -m'+' ispmgr'+' wireguard.user.suspend '+elid+' sok=ok'
		subprocess.run([url], shell=True)
i +=1

Появились ещё две непонятные надписи: ' sok=ok' и shell=True. Первая эмулирует нажатия кнопки «ОK» на форме подтверждения, которая выскакивает после попытки отключить юзера. Вторая говорит, что подпроцесс нужно выполнять в shell, это не очень безопасно, но подходит, когда нужно передать строку в url, чем мы, собственно и занимаемся.

Этап 5. Загрузка и запуск скрипта на Python в ispmanager

Когда скрипт готов, дело за малым — запустить его внутри ispmanager. Если вы планируете запускать скрипт с другого сервера, этот этап смело пропускайте. Я же использую mgrctl, складывать скрипт нужно на сервер, где установлен ispmanager.

Установка Python

Ispmanager поддерживает обработку Python, но предварительно её надо включить в конфигурации ПО.

По умолчанию установятся все доступные версии от 3.8 до 3.11. Лишние всегда можно отключить по двойному клику на Python
По умолчанию установятся все доступные версии от 3.8 до 3.11. Лишние всегда можно отключить по двойному клику на Python

Теперь панель поддерживает обработку Python. Как запустить сайт на нём, описано в документации. Но нам не нужен сайт, поэтому идём дальше.

Виртуальное окружение

Для запуска нашего скрипта необходимо создать виртуальное окружение — место, куда будут устанавливаться все нужные нам библиотеки. Я использую Python версии 3.11, поэтому разворачиваю такое же окружение. Для этого в shell-клиенте ispmanager нужно выполнить команду:

/opt/ispmanager/python3.11/bin/python3.11 -m venv <имя-вашего-окружения>

Чтобы работать с окружением, необходимо его активировать:

source <имя-вашего-окружения>/bin/activate

Установка библиотек и запуск

Внутри окружения работает pip и pip3, но есть нюанс. Библиотека gspread и так довольно вредная в установке, тут отказалась заводиться. Помогло прямое обращение к интерпретатору Python в следующем формате:

<имя-вашего-окружения>/bin/python3.11 pip3 install gspread

Важно: предварительно надо прожать f5, находясь на экране shell, чтобы выйти из окружения. Остальные модули установились через стандартные pip и pip3 внутри окружения.

После того как все модули установлены, можно загружать скрипт. Для этого идём в Менеджер файлов и складываем в нужную нам папку файл скрипта с расширением .py и файл credits.json, которые мы достали из Google. Я подключал логирование, поэтому рядом со скриптом положил файл и для него. Тоже с расширением .py. Складывать файлы в одну папку необязательно. 

Сохраняем путь до файла и возвращаемся в shell. Чтобы запустить файл, нужно воспользоваться командой:

<имя-вашего-окружения>/bin/python3.11 <путь до файла>

Если всё сделано правильно, запуск пройдёт удачно, а в консоли ничего не будет видно. Но зато будет видно в Google Sheets. Если у вас другая версия Python поменяйте 3.11 на нужный интерпретатор.

Пример заполнения данных. Цифры придуманы
Пример заполнения данных. Цифры придуманы

Если есть пользователи, подпадающие под критерии, на вкладке VPN в ispmanager они тоже отключатся.

Резюме

Я сознательно не стал убирать кусок с API-запросом, чтобы показать разные варианты взаимодействия с ispmanager. Но поскольку скрипт лежит на сервере с панелью, можно и запрос пользователей переписать на mgrctl. Если решите переписывать, не забудьте, что вывод из subprocess не передаётся в Python по умолчанию, его необходимо подхватывать с помощью отдельной команды.

По ходу работы я добавлял много дебага в код, причём по привычке командой print. Вам советую пользоваться модулем logging: он умеет писать в файл за пределами скрипта, что важно, когда программа выполняется в фоновом режиме.

Работа над статьёй помогла сделать код чище, но всё ещё возможна лишняя нагрузка на оперативку — сказывается моя неопытность в общем написании кода. Если у вас остались вопросы или появились советы по улучшению, буду рад обсудить их в комментариях.

А если статья была полезна, ставьте плюсик. Пусть больше людей увидят материал.

Комментарии (3)


  1. Veramilet
    29.09.2023 09:12
    +1

    Класс, спасибо, что поделился таким подробным описанием. Реализовано и правда классно, насколько сложно было впервые начать кодить без доп подготовки?


    1. Ave_Ls Автор
      29.09.2023 09:12

      На Питоне легче чем на Шарпе))) Сильно спасают библиотеки и большое количество топиков по теме.

      Основная сложность в том, что делая по гайдам ты зачастую не понимаешь что делаешь. Поэтому при возникновении проблем сложно понять что не так. Приходится влезать в вопросы вида "что такое XML", но тем интереснее.

      Разумеется, если стоит задача зарабатывать кодингом - такое баловство не подойдет, надо системно учиться. Но для моих целей хватило)


  1. Iron_Butterfly
    29.09.2023 09:12
    +1

    Понравилось про парсинг реквестов из лога и сборку API запроса из этого.

    Получение новых навыков всегда отлично происходит через конкретную практическую задачу.