Руководство для разработчиков

Примеры плагинов на PHP, Python, Node.js

При использовании интерпретируемых языков в первую очередь понадобится убедиться, что в системе есть нужный интерпретатор. Зачастую выполнение команды типа which php или which python или which node вернет вам путь к существующему интерпретатору, в каких-то случаях являющемуся частью дистрибутива ОС, в каких-то - установленному из ispmanager (например, в стандартной установке ispmanager по умолчанию включается поддержка PHP).

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

В статье про плагин “Hello, world!” был приведен пример простейшего обработчика на Bash-скрипте, а в разделе Структура и возможности плагинов подробно разобраны базовые приемы написания плагинов, такие, как описание структуры UI посредством XML, чтение значений полей формы из переменных окружения и взаимодействие с API ispmanager. Здесь приводится более сложный пример, чем “Hello, world!”, на трех популярных интерпретируемых языках - Python, Node.js и PHP, - который реализует следующие шаги:

  • Обработчик получает имя пользователя, который открыл страницу плагина, из переменной окружения AUTH_USER, а также значение поля формы с именем db из переменной PARAM_db - при начальной загрузке страницы такой переменной не будет;
  • Используя имя пользователя, обработчик с помощью утилиты mgrctl делает запрос к функции db API ispmanager, чтобы получить список баз данных, к которым есть доступ у данного пользователя панели;
  • В случае, если из переменной окружения PARAM_db ничего не получено, обработчик показывает форму, состоящую из выпадающего списка баз данных, полученных на прошлом шаге, и кнопки “Отправить”. Кнопка отправляет данные на ту же самую функцию dbselect, для которой мы создаем обработчик;

Если же переменная окружения PARAM_db не пустая, отображается текст, включающий имя выбранной БД (значение поля db, полученное из этой переменной).

Подключение обработчика

Чтобы протестировать любой из приведенных ниже обработчиков, нужно подключить его в ispmanager как обработчик и добавить соответствующую функцию в меню, это можно сделать, создав файл /usr/local/mgr5/etc/xml/ispmgr_mod_dblist.xml со следующим содержимым (см комментарий в коде к строке, которая должна быть изменена в зависимости от конкретного примера обработчика):

<?xml version="1.0" encoding="UTF-8"?>
<mgrdata>
    <mainmenu level="30">
        <modernmenu>
            <node name="dbselect_group" type="noname">
                <node name="dbselect" />
            </node>
        </modernmenu>
    </mainmenu>
    <!-- name="populateselect.php" или name="populateselect.js"
        для PHP и Node.js, соответственно -->
    <handler name="populateselect.py" type="xml">
        <func name="dbselect"/>
    </handler>
    <lang name="ru">
        <messages name="desktop">
            <msg name="modernmenu_dbselect">Select DB Demo</msg>
        </messages>
    </lang>
</mgrdata>

Данное XML-описание плагина добавит обработчик - файл populateselect.py (populateselect.php, populateselect.js) для функции dbselect и ссылку (“Select DB Demo”) на страницу с этой функцией в левое меню.

Теперь осталось создать файл обработчика - создайте файл с этим названием в папке /usr/local/mgr5/addon, присвойте ему права 750 (пользователь и группа root) и скопируйте туда код соответствующего обработчика из приведенных ниже.

Чтобы ispmanager применил добавленное вами XML-описание плагина, нужно выполнить в командной строке сервера команду:

pkill core

Эта команда нужно только при правках в XML-описании плагина, а изменения кода обработчика применяются сразу при следующем запросе к соответствующей функции.

Обработчик на Python

Обратите внимание, что приведен код на Python 2, для Python 3 могут потребоваться небольшие изменения кода.

#!/usr/bin/python
# -*- coding: utf-8 -*-

import os
import subprocess
import re

# получаем имя авторизованного пользователя
user = os.environ.get('AUTH_USER')

# получаем значение поля db из формы
db = os.environ.get('PARAM_db')

# если в поле db что-то есть, показываем текст,
# если в нем пусто, показываем селект и кнопку
if db is not None:
    formElements = """
    <field name="selected">
        <textdata name="selected_message" type="msg" />
    </field>
    """
else:
    formElements = """
    <field name="db">
        <select name="db"/>
    </field>
    <buttons>
        <button name="ok" type="ok" action="dbselect"/>
    </buttons>
    """

# получаем список баз данных, доступных данному пользователю
try:
    dbLines = subprocess.check_output(
        ["/usr/local/mgr5/sbin/mgrctl", 
        "-m", 
        "ispmgr", 
        "db", 
        "su={}".format(user)])
except subprocess.CalledProcessError as e:
    dbLines = e.output

# извлекаем из списка имена БД
dbs = [m for m in re.findall(r'name=([a-zA-Z0-9_]+)[^\n]*', dbLines)]

# готовим опции для селекта с именами БД
dbOptions = "".join(map(
    lambda d: '<val msg="yes" key="{}">{}</val>'.format(d, d), dbs))

xml = """<?xml version="1.0" encoding="UTF-8"?>
<doc lang="ru" func="dbselect">
    <metadata type="form">
        <form>
            {}
        </form>
    </metadata>
    <messages>
        <msg name="title">Выбор базы данных</msg>
        <msg name="db">Выберите базу данных</msg>
        <msg name="msg_ok">Отправить</msg>
        <msg name="selected_message">Выбрана БД: {}</msg>
    </messages>
    <slist name="db">
        {}
    </slist>
</doc>
"""
print(xml)

Обработчик на PHP

#!/usr/bin/php
<?php 
// переменная окружения - текущий user
$user = $_SERVER['AUTH_USER'];
// значения полей формы тоже приходят как переменные окружения
$db = $_SERVER['PARAM_db'];
// формируем metadata для формы
$form_xml = $db ? 
    '<field name="selected">
        <textdata name="selected_message" type="msg" />
    </field>' :
    '<field name="db">
        <select name="db"/>
    </field>
    <buttons>
        <button name="ok" type="ok" action="dbselect"/>
    </buttons>';
// спрашиваем в коммандной строке какие у нас есть базы данных
$db_output = `/usr/local/mgr5/sbin/mgrctl -m ispmgr db su=$user`;
// определяем имена баз данных
preg_match_all(
    "/name=([a-zA-Z0-9_]+)[^\n]*/",
    $db_output,
    $db_matches,
    PREG_PATTERN_ORDER
);
$db_names = $db_matches[1];
// формируем xml, описывающий опции для селекта
$db_options = array_map(fn($name) => 
    "<val msg=\"yes\" key=\"$name\">$name</val>", 
    $db_names);
$slist_xml = implode('', $db_options);
?>
<?php echo "<?" ?>xml version="1.0" encoding="UTF-8"?>
<doc lang="ru" func="dbselect">
    <metadata type="form">
        <form>
            <?php echo $form_xml; ?>
        </form>
    </metadata>
    <messages>
        <msg name="title">Выбор базы данных</msg>
        <msg name="db">Выберите базу данных</msg>
        <msg name="msg_ok">Отправить</msg>
        <msg name="selected_message">Выбрана БД: <?php echo $db; ?></msg>
    </messages>
    <slist name="db">
        <?php echo $slist_xml; ?>
    </slist>
</doc>

Обработчик на Node.js

Обратите внимание, что путь к интерпретатору Node.js в примере - это путь к node, который был установлен ispmanager, убедитесь, что в первой строке указан правильный путь, особенно если вы устанавливали Node.js из репозитория ОС или из внешнего источника, например, с сайта Node.js.

#!/usr/lib/ispnodejs/bin/node
const exec = require('child_process').exec;
// получаем имя авторизованного пользователя
const user = process.env.AUTH_USER;
// получаем значение поля db из формы
const db = process.env.PARAM_db;
// если в поле db что-то есть, показываем текст,
// если в нем пусто, показываем селект и кнопку
const formElements = db ? `
    <field name="selected">
        <textdata name="selected_message" type="msg" />
    </field>` : `
    <field name="db">
        <select name="db"/>
    </field>
    <buttons>
        <button name="ok" type="ok" action="dbselect"/>
    </buttons>
    `;
async function execute(command){
    return new Promise(
        res => exec(command, (error, stdout, stderr) => res(stdout) ));
}
function extractInfo(dblines) {
    return [...dblines.matchAll(/name=([a-zA-Z0-9_]+)[^\n]*/g)]
        .map(matches => matches[1]);
}
async function returnXml() {
    // получаем список баз данных, доступных данному пользователю
    const dbLines = await execute(
        `/usr/local/mgr5/sbin/mgrctl -m ispmgr db su=${user}`);
    // извлекаем из списка имена БД
    const dbs = extractInfo(dbLines);
    // готовим опции для селекта с именами БД
    const dbOptions = dbs.map(
        db => `<val msg="yes" key="${db}">${db}</val>`
    ).join('');
    // собираем и выводим итоговый XML
    console.log(`<?xml version="1.0" encoding="UTF-8"?>
    
    <doc lang="ru" func="dbselect">
        <metadata type="form">
            <form>
                ${formElements}
            </form>
        </metadata>
        <messages>
            <msg name="title">Выбор базы данных</msg>
            <msg name="db">Выберите базу данных</msg>
            <msg name="msg_ok">Отправить</msg>
            <msg name="selected_message">Выбрана БД: ${db}</msg>
        </messages>
        <slist name="db">
        ${dbOptions}
        </slist>
    </doc>`)
}
returnXml();