В статье описано создание обработчика (модуля обработки) услуг pmollama. Обработчик развёртывает локальные AI-модели Ollama в изолированных Docker-контейнерах на выделенном сервере провайдера. Рассмотрены:
- интеграция с Python SDK (Software Development Kit) BILLmanager;
- работа с дополнениями и параметрами;
- поддержка CPU- и GPU-режимов с ограничением ресурсов через NVIDIA MPS;
- типовые ошибки при создании обработчиков.
Общие сведения
Модули обработки управляют жизненным циклом услуги во внешних системах:
- интегрируемых платформах;
- API провайдеров;
- центрах сертификации;
- регистраторах доменов.
Стандартный модуль обработки BILLmanager состоит из исполняемого файла и XML-описания. Ниже приведены особенности, характерные для модуля pmollama.
Каталог установки: /usr/local/mgr5/processing/
XML-описание: /usr/local/mgr5/etc/xml/billmgr_mod_pm<имя>.xml
Обязательная команда:
features— возвращает XML-ответ с параметрами:<itemtypes>;<params>;<features>;<templates>(для SSL-сертификатов).
Основные команды (общие для большинства модулей обработки):
open— создание услуги;suspend— приостановка услуги;resume— возобновление услуги;close— удаление услуги;setparam— изменение параметров услуги или смена тарифа;prolong— продление услуги;sync_item— синхронизация статуса услуги с внешней системой;check_connection— проверка параметров подключения к внешней системе;tune_connection— модификация формы для добавления модуля обработки.
Специфичные команды для регистраторов доменов:
transfer— перенос домена;import— импорт существующих доменов;WHOIS— получение информации о владельце домена через WHOIS;update_ns— изменение DNS-серверов;get_contact_type— список типов контактов для доменной зоны;tune_service_profile— настройка контактов;validate_service_profile— проверка контактов;cancel_prolong— отмена автопродления;uploaddocs,uploadext— загрузка документов;contactverify,domainverify,checkdomaindoc— верификация документов.
Специфичные команды для SSL-сертификатов:
approver— получение email-адресов для подтверждения владения доменом;usercreate— создание аккаунта в центре сертификации;reopen— перевыпуск сертификата.
/usr/local/mgr5/processing/pmvpn --command open --item 42Описание XML
Наименование файла должно иметь вид billmgr_mod_pmXXX.xml, где XXX — имя модуля. Файл нужно скопировать в каталог /etc/xml относительно пути установки BILLmanager. Файл содержит описание самого модуля, а также описание дополнительных форм и сообщений.
Пояснения к командам XML-файла для модулей обработки услуг
Элемент features — выход
Возвращает XML-документ, содержащий описание параметров модуля обработки и поддерживаемых функций.
Особенности атрибутов:
nameу элемента itemtype указывается по внутреннему имени типа продукта или услуги;nameу элемента param должно соответствовать имени поля ввода в форме настроек для модуля обработки;cryptedсообщает о необходимости хранения параметра в базе данных в зашифрованном виде;nameу customparam отображается в списке выбора при добавлении дополнительного параметра;uniqueсо значениемyesуказывает на то, что параметр с выбранным внутренним именем может быть только один;defvalопределяет значение параметра по умолчанию (необязательный);nameу элемента feature указывает на поддерживаемую функцию модуля обработки.
Элемент get_suitable_module — вход и выход
Вход (stdin) — принимает XML-документ с параметрами услуги.
Выход — возвращает XML-документ вида:
<?xml version='1.0' encoding='UTF-8'?>
<doc>
<modules>
<module id="id1"/>
...
</modules>
</doc>Элемент check_connection — вход и выход
Вход (stdin) — принимает параметры подключения (всегда незашифрованные):
<?xml version='1.0' encoding='UTF-8'?>
<doc>
<processingmodule> <!-- Параметры обработчика. -->
<param1></param1>
...
</processingmodule>
</doc>Выход — возвращает XML-ошибку в формате BILLmanager или <doc/>:
<?xml version='1.0' encoding='UTF-8'?>
<doc>
<ok/>
</doc>Элементы tune_connection, tuning_param, usercreate, tune_changepassword — вход и выход
Вход — принимает текущий XML-файл формы (стандартное описание формы BILLmanager):
<doc>
<form>
<field name="api_url">
<input type="text" name="api_url" required="yes"/>
</field>
</form>
</doc>Выход — возвращает изменённый XML-файл. Вы можете добавлять поля, атрибуты, подсказки.
<doc>
<form>
<field name="api_url">
<input type="text" name="api_url" required="yes" placeholder="https://..."/>
</field>
<field name="extra_param">
<input type="text" name="extra_param"/>
</field>
</form>
</doc>Элемент import_pricelist — выход для разных subcommand
Для команды --subcommand available возвращает:
<doc>
<elem>
<id>...</id> <!-- Код тарифа в платформе или на стороне провайдера услуги. -->
<name>...</name> <!-- Наименование тарифа. -->
<name_ru>...</name_ru> <!-- Наименование тарифа в локализации ru. -->
<itemtype>...</itemtype> <!-- Внутреннее имя типа продукта тарифа.-->
</elem>
</doc>Для команды --subcommand pricelist возвращает:
<doc>
<elem>
<id>...</id>
<name>...</name>
<itemtype>...</itemtype>
<real_billtype>4</real_billtype> <!-- Фиксированное значение. -->
<internalname>...</internalname> <!-- Имя конфигурации тарифного плана. -->
<billdaily>...</billdaily> <!-- Значение on для ежедневного списания. -->
<minperiodtype>...</minperiodtype> <!-- Тип минимального периода заказа. -->
</elem>
</doc>Остальные subcommand: images, periods, details — формируют аналогичные XML-структуры с тегами name, intname, currency_code, period и т.д.
Элементы check_param и check_addon — вход
Вход — принимает XML-файл со старыми и новыми значениями параметров:
<doc>
<item> <!-- Список старых значений параметров услуги. -->
<param1>...</param1>
</item>
<newitem> <!-- Список новых значений параметров услуги. -->
<param1>...</param1>
</newitem>
</doc>Выход — возвращает <doc/> или ошибку.
Элемент transition_controlpanel — выход
Возвращает список панелей:
<doc>
<url></url> <!-- Ссылка для перехода. -->
<panelcount>2</panelcount>
<panelname keyname="vds_panel">VDS Panel</panelname>
<panelname keyname="vm_manager">VMmanager</panelname>
</doc>Элемент pingip — выход
<doc>
<ping_result>OK</ping_result> <!-- результат проверки: OK, если IP-адрес доступен. -->
</doc>Постановка задачи
Провайдер продаёт доступ к локальным AI-моделям через BILLmanager. При заказе услуги клиент получает изолированный Docker-контейнер с Ollama на выделенном сервере провайдера. Каждый контейнер доступен по уникальному URL-адресу с токеном авторизации.
Обработчик поддерживает работу в следующих режимах:
- CPU — контейнер из образа без CUDA, вычисления выполняются на центральном процессоре. GPU не требуется;
- GPU — контейнер из CUDA-образа с монтированием GPU в контейнер и ограничением доли вычислителя через NVIDIA MPS.
Режим выбирается в настройках обработчика.
Архитектура
BILLmanager
└─ Обработчик pmollama (Режим CPU или режим GPU)
└─ SSH на сервер (Один сервер = Один обработчик)
└─ docker run hwdsl2/ollama-server[:cuda] (Один контейнер на услугу)
├─ Caddy внутри контейнера: auth-прокси на порт OLLAMA_PORT (11434)
│ └─ reverse_proxy → ollama serve на 127.0.0.1:41434
├─ --cpus / --memory / --storage-opt (Лимиты из дополнений)
└─ Только GPU-режим:
--gpus device=N (round-robin по GPU)
--ipc=host + /tmp/nvidia-mps (Клиент MPS)
CUDA_MPS_ACTIVE_THREAD_PERCENTAGE (Доля SM, дополнение gpu_pct)
CUDA_MPS_PINNED_DEVICE_MEM_LIMIT (Лимит VRAM, дополнение vram_gb)Запрос клиента проходит через несколько слоёв, прежде чем попасть к модели Ollama:
Уровень 1. BILLmanager
BILLmanager — это ядро биллинга. Он хранит услуги, тарифы, пользователей и вызывает обработчик pmollama при оплате, приостановке, смене тарифа и т.д. BILLmanager управляет Docker и общается с сервером через обработчик.
Уровень 2. Обработчик pmollama
Обработчик — это скрипт, который:
- получает параметры услуги (дополнения, режим, настройки);
- подключается по SSH к серверу провайдера и запускает нужные команды Docker.
Уровень 3. Сервер провайдера
Физический или виртуальный сервер, на котором запущены контейнеры клиентов. Требования к серверу:
- к одному серверу должен быть привязан только один обработчик с одним набором SSH-параметров;
- на сервере должен быть установлен Docker, а в GPU-режиме — дополнительно NVIDIA Container Toolkit с поднятым демоном MPS;
- каждый контейнер получает свой порт из диапазона, который задаётся в настройках обработчика (параметр
base_port).
Уровень 4. Docker-контейнер услуги
Для каждой услуги используется отдельный контейнер из образа hwdsl2/ollama-server (CPU) или hwdsl2/ollama-server:cuda (GPU). Контейнер изолирован: у него свои лимиты ресурсов, свой том с моделями и свой API-ключ.
Лимиты передаются из дополнений тарифа с помощью параметров команды docker run:
--cpus— количество ядер (дополнениеcpu_count);--memory— объём оперативной памяти (дополнениеram_gb);--storage-opt size=— квота на диск (дополнениеstorage_gb).
Уровень 5. Внутри контейнера: Caddy и Ollama
Внутри контейнера работают два процесса:
- Ollama (ollama serve) слушает только на 127.0.0.1:41434 — порт недоступен извне контейнера;
- Caddy слушает порт 11434 и проксирует запросы на внутренний порт Ollama, требуя заголовок
Authorization: Bearer <токен>.
Благодаря такой архитектуре клиент получает уникальный URL с токеном, а Ollama защищён от прямого доступа извне.
Поэтому публичный URL услуги хранится как
https://…, но сам контейнер отдаёт HTTP на свой порт.Особенности GPU-режима
В режиме GPU к контейнеру добавляются специальные флаги и переменные окружения:
--gpus device=N— проброс конкретной видеокарты;--ipc=hostи монтирование /tmp/nvidia-mps — подключение контейнера к демону NVIDIA MPS, который работает на хосте. ПеременнаяCUDA_MPS_ACTIVE_THREAD_PERCENTAGE— доля потоковых мультипроцессоров (SM), которую может занимать контейнер. Задаётся дополнениемgpu_pct. ПеременнаяCUDA_MPS_PINNED_DEVICE_MEM_LIMIT="0=<N>G"— лимит видеопамяти в гигабайтах. Задаётся дополнениемvram_gb.
nvidia-cuda-mps-control -d) должен быть поднят на хосте заранее. Без него контейнер в GPU-режиме запустится, но ограничения gpu_pct и vram_gb работать не будут — модуль это видит и пишет предупреждение в лог.Как всё это связано: путь запроса
Путь клиентского запроса к API Ollama:
- Клиент отправляет HTTP-запрос на публичный URL услуги (например,
https://ai.example.com:11435/api/tags) с заголовкомAuthorization: Bearer <токен>. - Внешний прокси провайдера терминирует HTTPS и перенаправляет запрос на порт контейнера на хосте.
- Docker пробрасывает запрос на порт
11434внутри контейнера, где его принимает Caddy. - Caddy проверяет токен. Если токен верный, проксирует запрос на
127.0.0.1:41434, где слушает Ollama. - Ollama обрабатывает запрос и возвращает ответ по той же цепочке обратно клиенту.
pmollama выполняет от имени BILLmanager. Клиент работает только с API Ollama через публичный URL.Результат внедрения
- тип продукта
localaiс дополнениями:- CPU;
- RAM;
- disk;
- доля GPU;
- лимит VRAM;
- тонкая настройка Ollama;
- обработчик
pmollamaна Python с переключателем режима CPU или GPU; - полный жизненный цикл услуги через SSH:
- создание;
- пауза;
- возобновление;
- смена тарифа;
- продление;
- удаление;
- уникальный токен и URL-адрес для каждой услуги;
- сбор статистики использования ресурсов (CPU, RAM и диск) через команду
docker stats.
Жизненный цикл услуги и команды обработчика
Команды обработчика вызываются в ответ на действия пользователей и администраторов.
SDK: вызов команд и обработка аргументов
Раздел описывает, как BILLmanager запускает команды обработчика, передаёт аргументы и обрабатывает результаты. Понимание этого механизма помогает избежать типичных ошибок при разработке.
Точка входа и run()
BILLmanager запускает обработчик как обычный исполняемый файл и передаёт команду с помощью опции --command:
/usr/local/mgr5/processing/pmollama --command open --item 1234 --runningoperation 5678Внутри pmollama.py создаётся объект модуля и вызывается .run(). Метод run() находится в базовом классе billmgr.modules.base.Module и выполняет следующие действия:
def run(self) -> None:
args = self._parse_args() # 1. Разбираем argparse.
if args.test: # 2. --test/-T → "(c) ISPsystem.com" и выход
sys.stdout.write("(c) ISPsystem.com")
sys.exit(0)
self._before_run_command(args) # 3. Предобработка (features + проверка реализованности).
try:
self._run_command(args) # 4. Вызов команды.
except Exception as err:
exc.log_backtrace()
xml_err = err if isinstance(err, exc.XmlException) \
else exc.XmlException(err_type=args.command, err_value=str(err))
self._on_raise_exception(args, xml_err) # 5. Обработка ошибки.
sys.exit(1)Значительная часть ошибок происходит во время выполнения одного из шагов в коде выше.
Механизм вызова функции команды
Команды регистрируются в конструкторе модуля и сохраняются в словаре self._commands: Dict[str, Callable], где ключ — имя команды (open, close и т.д.). Шаг запуска команды (_run_command) выглядит следующим образом:
def _run_command(self, args):
cmd = self._commands.get(args.command)
if not cmd:
raise exc.XmlException("unknown command", "", args.command)
# Берём из всех argparse-аргументов только те, имена у которых совпадают с именами параметров функции команды:
params = {k: v for k, v in vars(args).items() if k in signature(cmd).parameters}
cmd(**params)SDK подставляет в функцию только те аргументы, которые были объявлены в сигнатуре. Имена параметров функции должны совпадать с dest соответствующих argparse-аргументов:
def open(item: int) -> None: # Функция получит --item
def set_param(item, runningoperation):# Функция получит --item и --runningoperation
def stat(module: int) -> None: # Функция получит --module / -m
def check_connection() -> None: # Функция не объявляет аргументов → ничего не получитЕсли параметр функции назван не так, как назван dest аргумента, функция вызовет ошибку. Например, при объявлении def open(item_id):
- SDK не найдёт параметр
item_idсреди argparse-аргументов (тамitem) и вызоветopen()без аргументов; - вызов завершится ошибкой
TypeError: open() missing 1 required positional argument.
Список аргументов, которые передаёт BILLmanager
Конструктор базового класса модуля ProcessingModule.__init__ регистрирует все возможные аргументы.
Имя параметра должно совпадать со значением из столбца dest: item, module, runningoperation. Частая ошибка: использовать имя для параметра функции, которое будет отличаться от dest аргумента. Например, если указать значение def open(item_id), то SDK будет искать item_id. Поскольку в argparse зарегистрирован item, функция будет вызвана без аргументов. Вызов завершится ошибкой TypeError: open() missing 1 required positional argument.
Обработка служебной команды features и валидация обязательных методов
На шаге предобработки ( _before_run_command в примере вызова объекта) модуль выполняет следующие функции:
- обработка служебной команды features. Команда
--command features— служебный запрос, который платформа BILLmanager отправляет обработчику, чтобы узнать список поддерживаемых им возможностей. SDK формирует XML-описание этих возможностей на основе зарегистрированных команд и выводит его вstdout.
def _before_run_command(self, args):
if args.command == "features":
self.features() # Формирует XML возможностей и выводит в stdout.
sys.exit(0)
for feat, cmd in self._commands.items():
if cmd is None: # Определяет порядок действий, если команда зарегистрирована, но обработчик не задан.
raise exc.XmlException("not_implemented", err_object="feature", err_value=feat)-
проверка реализации обязательных команд. Модуль на этапе инициализации (
ProcessingModule.init) присваивает значениеNoneдля обязательных команд жизненного цикла услуги:open;close;resume;suspend;setparam.
# Внутри ProcessingModule.__init__:
self._add_callable_feature(Feature.OPEN) # _commands["open"] = None
self._add_callable_feature(Feature.CLOSE) # _commands["close"] = None
self._add_callable_feature(Feature.RESUME) # _commands["resume"] = None
self._add_callable_feature(Feature.SUSPEND) # _commands["suspend"] = None
self._add_callable_feature(Feature.SET_PARAM) # _commands["setparam"]= NoneПеред выполнением любой команды метод _before_run_command обходит словарь зарегистрированных команд. Если хотя бы одна из них осталась со значением None, выполнение прерывается исключением not_implemented. Это означает, что разработчик обязан переопределить все пять обязательных команд, даже если специфика модуля не предполагает использование некоторых из них. Например, suspend или resume.
Способы регистрации команд и возможностей
SDK предоставляет три метода для регистрации команд и возможностей модуля. Выбор метода определяет, как модуль будет представлен в XML-описании (features-XML) и как платформа будет взаимодействовать с ним.
При обработке команды --command features SDK формирует XML-описание, которое включает:
- типы продуктов из
itemtypes; - множество
self._features(зарегистрированные возможности); - параметры из
get_module_param()(выводятся как<param>) иget_module_custom_param()(выводятся как<customparam>).
Завершение операции: post*-функции
Асинхронная команда не изменяет статус услуги автоматически. Чтобы платформа BILLmanager перевела услугу в требуемый статус и пометила текущую операцию как выполненную, после успешного выполнения команды вызовите соответствующую post*-функцию из библиотеки billmgr.misc:
Если после функции open не вызывается функция завершения misc.postopen(item), контейнер создаётся и работает, но услуга остаётся в статусе обработки. Платформа BILLmanager считает операцию незавершённой и повторно вызывает функцию
open. Э
то приводит к ошибке Conflict: The container name "/ollama_1234" is already in use при попытке запустить контейнер с тем же именем. Чтобы избежать этой ошибки, добавляйте misc.postopen(item) в конце функции open.
Ошибки, авто-повтор и идемпотентность
Если команда вызывает исключение, срабатывает обработка ошибок (_on_raise_exception):
def _on_raise_exception(self, args, err):
if not args.runningoperation:
return # Синхронные команды не вызываются повторно.
misc.save_runningoperation_error(args.runningoperation, err.as_module_error())
MAX_TRY = 10
res = db.get_first_record(
"SELECT trycount FROM runningoperation WHERE id = %s", args.runningoperation)
if res and res.as_int("trycount") >= MAX_TRY:
misc.create_manual_task(args.item, args.runningoperation, args.command)Поведение команд при ошибке различается:
- асинхронная операция повторяется автоматически — до 10 раз, после чего создаётся задача на ручную обработку. Команды должны быть идемпотентны: повторный запуск не должен ломаться на том, что часть работы уже сделана;
- синхронные команды (без
runningoperation) не повторяются. Их ошибку BILLmanager показывает сразу в форме — поэтому для них важно вернуть осмысленныйXmlException.
Чтобы обеспечить идемпотентность в close, suspend и resume все shell-команды обёрнуты в || true, а удаление тома идёт с raise_on_error=False:
ssh.run(f"docker stop {name} || true && docker rm {name} || true")close для уже удалённого контейнера — приводит к возникновению ошибки Error: No such container. Из-за того, что автоматически предпринимаются повторные попытки вызвать операцию, штатное закрытие вызовет 10 ошибок и потребует ручного вмешательства. Рекомендуется модифицировать логику удаления таким образом, чтобы повторные вызовы не приводили к ошибке, а завершались успешно, с сохранением целевого состояния системы (контейнер удалён).Возврат ошибки в интерфейс: XmlException и msgerror
Чтобы пользователь увидел понятный текст, а не низкоуровневые трассировки стека Python, вызывайте исключение типа billmgr.exception.XmlException c указанием содержательного идентификатора типа ошибки в параметре err_type:
raise XmlException("open_error") from eЗначение err_type (в примере выше open_error) используется для поиска в XML-секции <messages name="msgerror"> текста, соответствующего шаблону msg_error_<err_type>:
<msg name="msg_error_open_error">Не удалось запустить контейнер Ollama.</msg>Чтобы продублировать ошибку в стандартный поток (stdout), в точке входа модуля переопределите метод _on_raise_exception. Система отобразит её в синхронных формах:
def _on_raise_exception(self, args, err):
super()._on_raise_exception(args, err) # Запись в runningoperation и повторные попытки.
sys.stdout.write(err.as_xml()) # Вывод ошибки в форму.err.as_xml() в стандартный поток (stdout) одновременно и в обработчике, и в самой команде. Двойной вывод данных в формате XML нарушает работу парсера ответов BILLmanager. Выводите ошибку в одном месте — либо в методе _on_raise_exception, либо в команде.Тип продукта
Тип продукта (itemtype) определяет категорию услуги в BILLmanager. Он задаёт набор дополнений, параметров и поведение системы при заказе, продлении и изменении услуги. Без типа продукта невозможно создать тариф и привязать к нему обработчик. Подробнее см. статью Типы продуктов.
Стандартные типы (vds, vhost, domain) не подходят для обработчика pmollama, так как имеют собственную логику жизненного цикла и не содержат необходимых дополнений. Создайте собственный тип localai.
Создание типа продукта
Чтобы создать собственный тип продукта:
- Откройте веб-интерфейс платформы под учётной записью администратора.
- В главном меню перейдите в раздел Продукты → Типы продуктов → нажмите Создать.
- Заполните поля:
- Внутреннее имя:
localai; - Название: LocalAI (Ollama).
- Внутреннее имя:
Внутреннее имя localai — идентификатор. Передайте его в:
- код (
super().__init__(itemtypes={"localai"})); - конфигурацию XML (
<type name="localai"/>).
Значение должно совпадать с указанным в интерфейсе.
Связь типа продукта с модулем обработки
Чтобы установить связь типа продукта с модулем обработки в двух местах одновременно:
- В XML-файле модуля укажите элемент
<type name="localai"/>внутри раздела<plugin>. Это указывает BILLmanager, что данный обработчик поддерживает типlocalai.SDK подставляет тип в ответ на команду<plugin name="pmollama"> <group>processing_module</group> <params> <type name="localai"/> </params> </plugin>features, и BILLmanager сопоставляет данные. - В веб-интерфейсе платформы:
- Перейдите в раздел Интеграция → Обработчики услуг → нажмите Создать.
- Выберите модуль
pmollama. Модульpmollamaобрабатывает заказы тарифа типаlocalai. - Укажите параметры подключения к серверу и режим работы.
- В настройках тарифа выберите этот обработчик.
datacenter в модуле обработки. В модуле SDK эта возможность отсутствует. Добавьте её через дополнительное перечисление (Enum) и метод _add_feature(ExFeature.DATACENTER). Без указания этой возможности тариф с данным модулем обработки не отобразится у клиента в списке доступных для заказа услуг.Дополнения и параметры тарифа
В BILLmanager сущности, которые настраиваются у услуги, делятся на несколько категорий с разными правилами чтения.
Дополнения тарифа (измеримые ресурсы)
Дополнение — измеримый числовой ресурс, входящий в тариф (CPU, RAM, диск). При смене тарифа дополнения меняются. При создании тарифа для обработчика дополнения — это лимиты Docker-контейнера и параметры GPU.
Чтобы создать дополнение, перейдите в раздел Продукты → Типы продуктов → выберите localai → вкладка Содержание. Внутреннее имя дополнения в интерфейсе должно точно совпадать с именем, которое использует модуль обработки.
Чтение дополнений в команде open
Используйте функцию misc.itemaddons(item) из модуля SDK. Она выполняет SQL-запрос и возвращает словарь с дополнениями.
addons = misc.itemaddons(item)
# {"cpu_count": Addon(value="4", measure="pcs", bill_type=...),
# "ram_gb": Addon(value="16", measure="gb", ...), ...}При работе с функцией misc.itemaddons(item) учитывайте следующие особенности:
- экземпляр дополнения (
Addon) представляет собой именованный кортежNamedTuple(value, measure, bill_type). Значение доступно по индексу[0]или через атрибут.value, а единица измерения — по индексу[1]или через атрибут.measure; - значение хранится в виде строки, поэтому преобразуйте его в тип
intилиfloatвручную; - словарь содержит только заданные дополнения, поэтому проверяйте наличие ключа перед обращением к нему, чтобы предотвратить ошибку
KeyError.
Используйте следующий шаблон для безопасного чтения значений дополнений с указанием значений по умолчанию:
def get_addons(item: int) -> dict:
addons = misc.itemaddons(item)
def i(key, default):
return int(addons[key][0]) if key in addons else default
return {
"cpu_count": i(CPU_COUNT, CPU_COUNT_DEFAULT),
"ram_gb": i(RAM_GB, RAM_GB_DEFAULT),
"storage_gb": i(STORAGE_GB, STORAGE_GB_DEFAULT),
"gpu_pct": i(GPU_PCT, GPU_PCT_DEFAULT),
}Типичная ошибка: присваивать переменной cpu значение addons["cpu_count"] и передавать его в качестве параметра --cpus. :
- результатом будет именованный кортеж
Addon, а не числовое значение; - если дополнение не задано в тарифе, возникнет исключение
KeyError.
Всегда проверяйте наличие ключа в словаре и извлекайте значение с указанием значения по умолчанию: int(addons[key][0]) if key in addons else default.
Используйте функцию для чтения значений необязательных дополнений. Функция возвращает None, если значение отсутствует.
def get_ollama_tuning(item: int) -> dict:
addons = misc.itemaddons(item)
def opt(key):
if key not in addons:
return None
try:
v = int(addons[key][0])
except (TypeError, ValueError):
return None
return v if v > 0 else None
return {
"OLLAMA_MAX_LOADED_MODELS": opt(OLLAMA_MAX_LOADED_MODELS),
"OLLAMA_NUM_PARALLEL": opt(OLLAMA_NUM_PARALLEL),
"OLLAMA_CONTEXT_LENGTH": opt(OLLAMA_CONTEXT_LENGTH),
}Параметр тарифа ollama_models (строка)
Список моделей через запятую представляет собой строку и не может быть числовым дополнением. Задайте этот параметр тарифа через динамические метаданные pricelist.pmollama.localai (требует включения опции PRICELIST_DYNAMIC_SETTINGS). Для чтения значения используйте отдельную функцию:
def get_default_models(item: int) -> str:
params = misc.get_pricelist_params(misc.iteminfo(item)["pricelist"])
return (params.get(OLLAMA_MODELS_PARAM) or "").strip()itemaddons. Дополнения — только измеримые числовые ресурсы. Строки и флаги конфигурации читаются через misc.get_pricelist_params(pricelist_id) или misc.itemparams(item).Параметры услуги
Параметр услуги — значение, которое модуль сохраняет для конкретной услуги при выполнении команд (open, setparam):
container_name;container_url;container_token(хранится зашифрованным).
Для записи используется функция misc.save_param, для чтения — функция misc.itemparams. Расшифровка зашифрованных параметров происходит автоматически:
misc.save_param(item, param="container_name", value=name)
misc.save_param(item, param="container_url", value=container_url)
misc.save_param(item, param="container_token", value=token, crypted=True)
params = misc.itemparams(item)
token = params.get("container_token")Чтобы настроить параметры услуги, перейдите в раздел Продукты → Типы продуктов → выберите localai → вкладка Параметры. Подробнее см. Параметры типа продукта.
Параметры обработчика (подключение и режим)
Эти параметры задают при создании модуля обработки. Система хранит их на уровне модуля. Для чтения используйте функцию misc.get_module_params(module_id). Система автоматически расшифровывает зашифрованные поля. Например, пароль:
module_id = misc.get_item_processingmodule(item)
params = misc.get_module_params(module_id)
ssh_host = params["ssh_host"]
ssh_user = params["ssh_user"]
ssh_password = params["ssh_password"]
base_port = int(params.get("base_port", "11434"))
public_url = params["public_url"]
gpu_mode = params.get("gpu_mode")get_module_params внутри метода check_connection. На этапе проверки соединения система может не сохранить данные модуля обработки. В методах check_connection и tune_connection данные приходят из входного XML-файла, а не из БД.Справочная информация: типы параметров и функции для их чтения
Образ hwdsl2/docker-ollama и режимы CPU/GPU
Модуль использует готовый образ hwdsl2/docker-ollama вместо стандартного образа ollama/ollama. Структура этого образа определяет переменные окружения и тома, которые необходимо указать в команде docker run.
Варианты образа
Модуль автоматически выбирает образ в зависимости от режима работы обработчика с помощью функции consts.image_for_mode().
Использование Caddy в качестве прокси-сервера для аутентификации и работа с двумя портами
Внутри контейнера выполняются два процесса:
- Процесс
ollama serveпринимает соединения только по адресу127.0.0.1:41434. Порт недоступен извне. - Процесс
caddyпринимает соединения на портуOLLAMA_PORT(по умолчанию11434) и перенаправляет запросы на внутренний порт. Для всех маршрутов, кроме корневого, требуется наличие заголовкаAuthorization: Bearer <api_key>.
Внешний доступ предоставляется к порту Caddy. В команде docker run необходимо указать параметр -p {host_port}:11434. При отсутствии корректного токена Caddy отклоняет запрос, возвращая код ответа 401 Unauthorized.
public_url необходимо сохранять с префиксом https://, несмотря на то, что сам контейнер передает данные на свой порт по протоколу HTTP.API-ключ
Платформа читает ключ авторизации из переменной окружения OLLAMA_API_KEY и сохраняет его в файл /var/lib/ollama/.api_key. Если переменная не задана, образ самостоятельно генерирует случайный ключ и сохраняет его в тот же файл.
Модуль обработки задаёт значение переменной OLLAMA_API_KEY с помощью функции misc.random_string(32). Это позволяет модулю сразу получить ключ и исключает необходимость его чтения из контейнера после запуска.
Том данных
Модели и ключ авторизации сохраняются в каталоге /var/lib/ollama. Смонтируйте именованный том в этот каталог с помощью параметра -v ollama_{item}:/var/lib/ollama. Данные в томе сохраняются при пересоздании контейнера. Модуль использует эту особенность при выполнении команды setparam для пересоздания контейнера.
Переменные окружения, используемые образом
Скрипт запуска образа передает процессу Ollama только следующие переменные окружения, если они заданы:
Переменные, которые не поддерживаются скриптом запуска и не будут переданы процессу:
OLLAMA_GPU_OVERHEAD— скрипт запуска не передаёт эту переменную процессуollama serve;- Ограничения GPU на уровне
cgroups— Docker не поддерживает их без использования технологий MIG или MPS.
Переключатель режима и зависимости
Режим работы определяется select-параметром обработчика gpu_mode со значениями cpu или gpu. Варианты для выбора формируются динамически командой tune_connection. Модуль обработки приводит значение параметра к стандартному виду и выполняет следующие действия:
-
в обоих режимах выбирает соответствующий Docker-образ (
image_for_mode):- для
cpu—hwdsl2/ollama-server; - для
gpu—hwdsl2/ollama-server:cuda;
- для
- в режиме
gpuдополнительно формирует параметры запуска контейнера с учётом ограничений ресурсов графического процессора (gpu.build_run_flags).
Распределение ресурсов графического процессора между контейнерами
В режиме gpu ресурсы графического процессора распределяются между контейнерами следующими способами:
- Циклическое распределение графических процессоров (Round-robin по GPU). Используется параметр
--gpus device=N, где значение N вычисляется как остаток от деления идентификатора услуги на общее количество графических процессоров:N = item % число_GPU.
Где item — целочисленный идентификатор услуги в BILLmanager. - Доля потоковых мультипроцессоров (SM) через технологию NVIDIA MPS. Задается с помощью переменной окружения
CUDA_MPS_ACTIVE_THREAD_PERCENTAGE, значение которой берётся из дополненияgpu_pct. - Ограничение объема видеопамяти через MPS. Задается с помощью переменной окружения
CUDA_MPS_PINNED_DEVICE_MEM_LIMIT, значение которой формируется на основе дополненияvram_gb:CUDA_MPS_PINNED_DEVICE_MEM_LIMIT="0=<N>G" (дополнение
vram_gb).
Технология MPS представляет собой фоновый процесс на хосте (nvidia-cuda-mps-control -d), к которому контейнеры подключаются в качестве клиентов. Контейнерный образ не устанавливает MPS и не монтирует сокеты. Поэтому в режиме gpu, если фоновый процесс MPS запущен на хосте, модуль добавляет при запуске контейнера следующие параметры:
--ipc=host, -v /tmp/nvidia-mps:/tmp/nvidia-mps и -e CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mpsГде:
--ipc=host— контейнер использует пространство имён IPC хоста. Это позволяет процессам внутри контейнера взаимодействовать с фоновым процессом MPS через разделяемую память и именованные каналы;-v /tmp/nvidia-mps:/tmp/nvidia-mps— монтирует каталог с сокетами MPS с хоста в контейнер по тому же пути. Обеспечивает доступ к управляющим сокетам фонового процесса MPS;-e CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps— задаёт переменную окружения, указывающую CUDA-приложениям путь к каталогу с каналами взаимодействия с MPS.
Если фоновый процесс не запущен, модуль запускает контейнер с графическим процессором без ограничений и записывает соответствующее предупреждение в журнал событий.
XML-метаданные модуля
Файл xml/billmgr_mod_pmollama.xml описывает, как обработчик выглядит в платформе:
- форму настроек;
- форму тарифа;
- локализацию;
- тексты ошибок.
Имя файла должно соответствовать шаблону billmgr_mod_<binaryname>.xml, где <binaryname> — имя исполняемого файла обработчика (pmollama).
<plugin name="...">, имя исполняемого файла, значение binaryname и префиксы метаданных должны совпадать. Используйте единое имя, например pmollama. Любое расхождение приводит к ошибке поиска формы или делает модуль обработки недоступным в интерфейсе.Назначение XML-элементов
Элемент <plugin> регистрирует модуль обработки. Вложенный элемент group со значением processing_module относит его к модулям обработки услуг. Указание элемента <type name="localai"/> является обязательным.
Элемент <metadata> с именем processing.edit.pmollama определяет поля формы настроек модуля обработки. Значения этих полей считываются функцией misc.get_module_params(). Атрибут private со значением yes скрывает введённое значение в интерфейсе после сохранения.
Элемент <metadata> с именем pricelist.pmollama.localai определяет поля формы тарифного плана. Имя этого элемента формируется по шаблону pricelist.<binaryname>.<itemtype>.
Секция <messages> с именем msgerror содержит тексты сообщений об ошибках. Имя тега сообщения должно включать префикс msg_error_ и точно соответствовать значению параметра err_type, которое передается при генерации исключения XmlException("open_error").
Соответствие имён в конфигурации и коде
get_module_param(). BILLmanager сохраняет в БД только те параметры обработчика, которые перечислены в get_module_param(). Поэтому параметры gpu_mode, base_port, public_url и остальные должны присутствовать как в XML-форме, так и в функции get_module_param().Создание модуля на Python SDK
Структура проекта
pmollama/
├── install.sh # Установочный скрипт.
├── Makefile # Размещение файлов по каталогам в /usr/local/mgr5
├── requirements.txt # Зависимости модуля для установки в виртуальное окружение.
├── pmollama.py # Точка входа, содержит класс модуля и регистрацию команд.
├── ollama/ # Пакет с логикой, платформа размещает в lib/python/ollama.
│ ├── __init__.py
│ ├── consts.py # Константы.
│ ├── gpu.py # Построение флагов Docker для GPU и MPS.
│ ├── misc.py # Функции чтения для дополнений и параметров.
│ ├── ssh.py # Обёртка для библиотеки paramiko.
│ └── commands/ # Каталог с файлами, каждый файл реализует одну команду.
└── xml/
└── billmgr_mod_pmollama.xmlРазделение на пакет ollama/ и файл точки входа pmollama.py необходимо для корректной установки. Платформа размещает точку входа в каталоге processing/, а пакет — в каталоге lib/python/.
Точка входа: pmollama.py
Ключевые моменты точки входа:
- укажите тип продукта с помощью
super().__init__(itemtypes={"localai"}); - зарегистрируйте обязательные команды через
_add_callable_feature(Feature.X, fn); - определите параметры в
get_module_param(), где ключи совпадают с атрибутамиnameполей формы, а параметрssh_passwordсодержит значение{"crypted": "yes"}; - переопределите
_on_raise_exception, чтобы вывести текст ошибки в стандартный поток (stdout); - обеспечьте перезапуск в виртуальном окружении, где функция
os.execvзаменяет текущий процесс интерпретатором из виртуального окружения.
sys.path.insert(0, "/usr/local/mgr5/lib/python") перед импортом модулей billmgr.*. В этом случае инструкция import billmgr.modules.processing вызовет ошибку ModuleNotFoundError.Подробное описание команд
Общая форма команды: функция получает от SDK объявленные аргументы, выполняет действия через SSH, при необходимости сохраняет параметры услуги и обязательно завершается вызовом функции семейства post*-.
Ниже приведены примеры реализации команд:
open;close;suspend;resume;setparam;stat;check_connection;tune_connection.
Код соответствует правилам идемпотентности, использования функции misc.post* и корректного чтения дополнений, описанным в предыдущих разделах.
Полные тексты команд доступны в репозитории pmollama. Каждая команда должна соответствовать ключевым требованиям:
- идемпотентность (повторный вызов не вызывает ошибок);
- обязательный вызов функции семейства
post*в конце выполнения; - генерация информативного исключения
XmlExceptionпри ошибках.
Сборка и установка модуля
Установка состоит из следующих этапов:
- Установка зависимостей в изолированное виртуальное окружение (venv).
- Размещение файлов по каталогам в /usr/local/mgr5 с помощью
Makefile. - Сброс кеша BILLmanager.
requirements.txt
paramiko==3.5.0Файл содержит только те зависимости, которые нужны модулю дополнительно к SDK. Эти пакеты устанавливаются в виртуальное окружение, а не в системный интерпретатор Python.
Makefile
MGR = billmgr
PLUGIN = pmollama
BASE ?= /usr/local/mgr5
SRC = $(shell pwd)
VENV_PATH = $(SRC)/venv-pmollama
.PHONY: dist-prepare copy-lib
dist-prepare: $(DISTDIR)/processing/pmollama copy-lib
$(DISTDIR)/processing/pmollama: $(SRC)/pmollama.py
@mkdir -p $(DISTDIR)/processing/
cp -f $(SRC)/pmollama.py $(DISTDIR)/processing/pmollama
sed -i '1s|^#!.*|#!$(VENV_PATH)/bin/python3|' $(DISTDIR)/processing/pmollama
chmod 755 $(DISTDIR)/processing/pmollama
copy-lib:
@rm -rf $(DISTDIR)/lib/python/ollama
@mkdir -p $(DISTDIR)/lib/python/ollama
cp -rf $(SRC)/ollama/. $(DISTDIR)/lib/python/ollama/
@find $(DISTDIR)/lib/python/ollama -name __pycache__ -type d -prune -exec rm -rf {} +
include $(BASE)/src/isp.mkinstall.sh
#!/usr/bin/env bash
. /usr/local/mgr5/lib/pkgsh/core_pkg_funcs.sh
InstallDeps() {
Info "Installing dependencies..."
PKGS="billmanager-plugin-python-libs python3-pip make"
python3 -m venv venv-pmollama
"./venv-pmollama/bin/pip3" install -r requirements.txt
}
InstallModule() {
Info "Installing pmollama..."
DESTDIR=/usr/local/mgr5/src/pmollama
mkdir -p "${DESTDIR}" && cp -rfa ./* "${DESTDIR}"/
cd "${DESTDIR}" && make install
}
InstallDeps
InstallModuleПроверка установки
# 1. Сброс кеша XML и перезапуск BILLmanager.
/usr/local/mgr5/sbin/mgrctl -m billmgr exit
# 2. Проверка команды features вручную.
/usr/local/mgr5/processing/pmollama --command featuresmgrctl -m billmgr exit.Подготовка сервера и тестирование
Порты
Откройте диапазон портов для работы контейнеров:
ufw allow 11434:12000/tcpCPU-режим
Для работы в режиме CPU установите платформу Docker:
curl -fsSL https://get.docker.com | shGPU-режим
Для работы в режиме GPU установите Docker, драйвер NVIDIA, NVIDIA Container Toolkit и службу MPS.
Интерфейс (UI)
Иконка модуля
Иконка отображается в списке модулей обработки и в маркетплейсе. Исходный файл размещается в каталоге dist/skins/common/plugin-logo/billmanager-plugin-pmollama.png
Скрипт сборки isp.mk копирует файл в целевой каталог: в /usr/local/mgr5/skins/common/plugin-logo/billmanager-plugin-pmollama.png. Требования к файлу:
- формат PNG с прозрачным фоном;
- имя файла строго соответствует значению binaryname (billmanager-plugin-pmollama.png). Например, для модуля
pmollamaимя иконки будет billmanager-plugin-pmollama.png.
Иконки для кнопок в XML-формах
Платформа BILLmanager использует встроенный SVG-спрайт. Имя иконки для кнопки панели инструментов указывают в атрибуте @img.
<metadata name="localai" type="list">
<toolbar>
<toolbtn func="localai.open" name="open" img="t-on" type="new"/>
<toolbtn func="localai.suspend" name="suspend" img="t-lock" type="group"/>
</toolbar>
</metadata>Диагностика и ошибки
Если система работает некорректно, проверьте следующие пункты. Большинство проблем относится к одной из описанных ниже категорий.
Модуль обработки недоступен или тарифный план нельзя заказать
- убедитесь, что имя плагина в XML-файле, имя исполняемого файла в каталоге processing, значение
binarynameи префиксы метаданных полностью совпадают. Например,pmollama; - проверьте, что элемент type в XML-файле и параметр itemtypes в коде соответствуют внутреннему имени типа продукта. Например, localai;
- для пользовательского типа продукта должна быть объявлена опция
datacenter. Без её указания тарифный план не отобразится в списке доступных для заказа; -
убедитесь, что после внесения изменений в XML-файлы выполнена команда сброса кеша:
mgrctl -m billmgr exit - проверьте, что ручной запуск модуля с параметром
--commandfeatures возвращает корректные значения типов продуктов, возможностей и параметров (itemtype, features и params).
Команда завершается с ошибкой not_implemented
- убедитесь, что зарегистрированы все пять обязательных команд:
open,close,suspend,resume,setparam.
Команда завершается с ошибкой TypeError: ... missing argument
Убедитесь, что:
- имя параметра функции соответствует
dest:item,module,runningoperation,ip_id(неip); - имя файла или функции соответствует значению, переданному в
import_func(...).
Услуга создаётся, но остаётся в промежуточном состоянии или повторяется
Убедитесь, что:
- в конце команды
openесть вызовmisc.postopen(item); - команды идемпотентны:
- команды
docker stopиrmвыполняются с оператором|| true; - удаление тома выполняется с параметром
raise_on_error=False.
- команды
Параметры не читаются или не сохраняются
Убедитесь, что:
- все поля формы обработчика перечислены в
get_module_param(); - дополнения читаются с проверкой наличия ключа
int(addons[key][0]) if key in addons else default; - в методах
check_connectionиtune_connectionданные берутся из входного XML-файла, а не из функцииget_module_params.
Связанные статьи: