Подключаемые интеграции: панели и действия в карточках

Механизм показа живых данных из внешних ИС прямо в карточках объектов инвентаризации и выполнения действий во внешних ИС (отправка SMS, сброс пароля). Не путать с интеграциями через REST API — там внешние скрипты ходят в инвентаризацию, здесь наоборот: инвентаризация обращается во внешние системы.

Архитектура и контракт провайдера (для разработчиков) — в docs/dev/integrations.md.

Как это работает

  • Провайдер — подключаемый модуль одной интеграции. Включённые провайдеры перечисляются в params-local.php (ключ integrations), выключенная интеграция не оставляет следов в интерфейсе.
  • Панели — блоки с данными внешней ИС в карточке объекта. Страница никогда не ждёт внешнюю систему: сразу показывается прошлый результат из кэша (приглушённо), свежие данные подгружаются фоном; недоступность внешней ИС даёт компактную заглушку, а не ошибку страницы. Кэш — файлы в runtime/integrations_cache/, общий на инстанс.
  • Действия — кнопки в блоке интеграций (или иконки у атрибутов), открываются в модальном окне. Каждое выполнение журналируется.
  • Авторизация во внешних ИС: чтение и обычные действия — от сервисной учётки из конфига (пользователи инвентаризации никуда не логинятся); именные действия (сброс пароля AD) запрашивают личные учётные данные исполнителя на один запрос — они нигде не сохраняются.

Секреты (токены, пароли сервисных учёток) живут только в params-local.php, в БД они не попадают.

Включение

Добавить провайдеров в params-local.php и создать RBAC-права командой php yii rbac/init (право view-integration-<id> — видеть панели, edit-integration-<id>-<действие> — выполнять действие; раздаются ролям через штатный интерфейс /rbac/). Доступ к интеграциям следует той же модели авторизации, что и остальное приложение (useRBAC/authorizedView, см. Настройка): панель — как «просмотр», действие — как «изменение». В полностью открытом режиме (по умолчанию) интеграции доступны всем; включение RBAC/аутентификации ограничивает их ровно так же, как обычные операции.

Кому уже доступно при включённом RBAC. Пользователю с глобальным правом edit доступны все действия интеграций (SMS, сброс пароля), с глобальным view — все панели. Отдельные права *-integration-* нужны лишь для тонкой настройки (например, дать хелпдеску сброс пароля, не выдавая глобальный edit).

'integrations' => [

    // Отправка SMS через шлюз (иконки у телефонов в карточке сотрудника
    // + форма отправки на произвольный номер)
    'sms' => [
        'class' => \app\components\integrations\providers\SmsProvider::class,
        'url' => 'https://sms-gw.local/send?phone={phone}&text={text}',
        // Шлюз часто отвечает 200 и при отказе, поэтому ответ разбирается:
        // по умолчанию отказом считается ответ с маркерами ERROR, FAIL,
        // NOT_FOUND, NO_MESSAGE_GIVEN, DENIED, INVALID. Если у вашего шлюза
        // другой формат - задайте regexp успешного ответа:
        //'successPattern' => '/^OK\b/i',
    ],

    // Панель телефонии в карточке VoIP-телефона: прогрессивный статус
    // (в БД / в Asterisk / зарегистрирован / онлайн) с 4 индикаторами,
    // IP и модель телефона, кнопка перехода в Web-UI абонента,
    // дублирования вызова. Работает через REST API приложения
    // ast22-phones (endpoint /api/v1/subscribers/status).
    'pbx' => [
        'class' => \app\components\integrations\providers\HttpTemplateProvider::class,
        'title' => 'Телефония',
        'appliesTo' => ['model' => \app\models\Techs::class, 'attribute' => 'isVoipPhone'],
        'binding' => '{phone}',
        // request - backend-URL, достижимый С СЕРВЕРА ARMS:
        'request' => 'http://phones.local:8080/api/v1/subscribers/status?extension={binding}',
        'headers' => ['Authorization' => 'Bearer <токен сервисного пользователя>'],
        // таймаут запроса (по умолчанию 5с; телефония с AMI к Asterisk
        // может отвечать дольше - поднимите, если панель «недоступна»):
        'timeout' => 10,
        // Кнопка перехода в Web-UI телефонии появляется автоматически -
        // база берётся из host:port у request. web указывайте ЯВНО только
        // если браузерный адрес телефонии отличается от backend-адреса
        // request (напр. request идёт на имя контейнера, а браузер - на
        // localhost):
        //'web' => 'http://phones.local:8080',
        // путь абонента в Web-UI ({id} = subscriber.id), при необходимости
        // подгоните под роутинг вашей телефонии:
        'webSubscriber' => '/subscriber/view?id={id}',
        'panel' => ['title' => 'Телефония', 'ttl' => 30,
            'template' => '@app/components/integrations/providers/views/pbx/status.php'],
    ],

    // Учётка ActiveDirectory в карточке сотрудника: панель-справка
    // (статус учётки, смена/истечение пароля, последний вход) и действия
    // с учёткой. Использует уже настроенный ldap-компонент
    // (config/ldap.php), отдельной учётки не требует.
    //
    // Действия - кнопки в панели, появляются при включённом провайдере
    // 'sms' по живому состоянию учётки в AD:
    // - «сбросить пароль» - у найденной учётки;
    // - «создать учётную запись» (нужен usersOu) - у активного
    //   сотрудника, чьей учётки в AD нет (в том числе вовсе без логина -
    //   он предгенерируется по регламенту сквозных учёток: «фамилия.и»
    //   транслитом, не более 12 знаков (ограничение SAP; не влезло -
    //   обрезка с конца вместе с разделительной точкой), однофамильцам
    //   добавляется номер: smirnov.a2, popandopolo7); в форме: логин
    //   (занятость проверяется при открытии), дерево OU, группы; после
    //   создания логин записывается в карточку сотрудника. Атрибуты
    //   учётки заполняются строго по схеме скрипта синхронизации
    //   inventory-to-ad.ps1 (ФИО, должность, подразделение, организация,
    //   ИНН, табельный, org_id, почта, телефоны в формате синхронизации) -
    //   очередной прогон синхронизации такую учётку не переписывает;
    // - «восстановить учётную запись» (нужны usersOu+dismissedOu) - у
    //   отключённой учётки в контейнере уволенных (куда её переносит
    //   скрипт увольнения, зеркаля путь): включение + новый пароль +
    //   переезд обратно по зеркальному пути (можно выбрать другое OU).
    //
    // Все действия именные: исполнитель вводит СВОИ учётные данные AD
    // (нужны делегированные права: Reset Password; Create user objects
    // на OU; Write members на группах). Пароль всегда генерируется
    // автоматически (по умолчанию «произносимый» - проще продиктовать;
    // можно выбрать случайный), соответствует парольной политике и НЕ
    // показывается администратору - его узнаёт только пользователь из
    // SMS (поэтому смена при входе не требуется). Перед отправкой SMS
    // выполняется НЕДЕСТРУКТИВНАЯ предпроверка (без записи в AD): верны
    // ли креды исполнителя и есть ли у него нужные права - чтобы не
    // отправить SMS впустую; если после этого SMS не доставлено - в AD
    // ничего не меняется.
    'ad' => [
        'class' => \app\components\integrations\providers\AdUserProvider::class,
        // AD опрашивается при каждом открытии карточки (устаревший статус
        // учётки недопустим). Задайте cacheTtl в секундах, только если
        // контроллер домена далеко и панель заметно тормозит:
        //'cacheTtl' => 300,
        // настройки пароля/SMS (общие для всех действий):
        //'defaultLength' => 12, //длина пароля по умолчанию в форме
        //'smsText' => 'Ваш новый пароль: {password}',
        //'sms' => 'sms', //id SMS-провайдера, если он назван иначе
        // создание и восстановление учёток: пары корней «рабочий ↔
        // уволенные» - строго те же, что в конфиге скрипта увольнения
        // ($inventory2ad_sync в ad-usermanagement: u_OUDN/f_OUDN).
        // Увольнение переносит учётку из users-корня в dismissed-корень
        // той же пары с сохранением подпути; восстановление зеркалит
        // строго обратно в рамках своей пары. В форме создания доступны
        // поддеревья всех users-корней (сотрудники + внешние контрагенты):
        //'ouPairs' => [
        //    ['users' => 'OU=Пользователи,DC=corp,DC=local',
        //     'dismissed' => 'OU=Азимут,OU=Уволенные,DC=corp,DC=local'],
        //    ['users' => 'OU=External,DC=corp,DC=local',
        //     'dismissed' => 'OU=External,OU=Уволенные,DC=corp,DC=local'],
        //],
        // одна пара может быть задана и просто скалярами:
        //'usersOu' => 'OU=Пользователи,DC=corp,DC=local', //корень рабочих учёток (включает создание)
        //'dismissedOu' => 'OU=Уволенные,DC=corp,DC=local', //корень уволенных (включает восстановление)
        //'groupsOu' => 'OU=Группы,DC=corp,DC=local', //где искать группы для формы (не задан = весь каталог)
        //'defaultGroups' => ['Пользователи JIRA'], //предвыбранные группы (имена или DN)
        //'upnSuffix' => null, //суффикс UPN (не задан = account_suffix ldap-компонента)
    ],

    // Справка ActiveDirectory в карточке ОС: где учётка компьютера лежит
    // в дереве AD (путь по OU) и в каких группах состоит - по группам
    // раздаются политики и доступы. Тот же ldap-компонент, что и у 'ad'
    'ad-comp' => [
        'class' => \app\components\integrations\providers\AdComputerProvider::class,
        // по умолчанию опрашиваются только Windows-ОС (у остальных учётки
        // компьютера в AD обычно нет):
        //'windowsOnly' => false,
    ],

    // Панель Zabbix в карточке ОС/оборудования: базовые метрики узла
    // (аптайм, загрузка CPU и памяти, заполнение дисков), активные
    // проблемы (сработавшие триггеры) и ссылка на узел в Zabbix.
    // Привязка объекта к узлу - hostid в external_links (ключ
    // Zabbix.hostid), его пишет скрипт синхронизации arms.zabbix; пока
    // hostid не записан, панель ищет узел по FQDN/имени (в БД не пишет).
    // Плюс колонка «доступность + аптайм» в списках ОС/оборудования:
    // скрыта по умолчанию, включается в персонализации таблицы
    // (шестерёнка над списком); показывается только у объектов с
    // записанным hostid (поиска по именам в списках нет).
    'zabbix' => [
        'class' => \app\components\integrations\providers\ZabbixProvider::class,
        'api' => 'https://zabbix.local/zabbix/api_jsonrpc.php',
        'token' => '<API-токен сервисного пользователя Zabbix>',
        'web' => 'https://zabbix.local/zabbix', //для ссылки на узел
        //'verifySsl' => true, //по умолчанию сертификат не проверяется
        //'metrics' => false, //не показывать аптайм/CPU/память/диски (и аптайм в колонке списков)
        //'staleAfter' => 600, //с какого возраста данных писать «устарели»
        //'cacheTtl' => 60,
        //'cellTtl' => 30, //свежесть ячеек колонки в списках (мин. 15)
        // Одна карточка вместо двух: спрятать отдельную карточку Zabbix
        // и встроить её содержимое в «Постановку на мониторинг» (под
        // вердиктом; рисуется, когда узел на мониторинге или заведён в
        // Zabbix - у прочих узлов пропадает «узел не найден»). Требует
        // включённого 'zabbix-sync':
        //'embedded' => true,
    ],

    // Панель «Постановка на мониторинг» в карточке ОС/оборудования:
    // попадёт ли узел в Zabbix при синхронизации arms.zabbix и в каком
    // виде. В карточке - бейдж вердикта (будет добавлен / на мониторинге
    // / не ставится); клик по нему открывает окно с причинами и
    // совпавшими правилами, «подробно» там же раскрывает полный журнал:
    // все проверенные правила и на каком условии срезались.
    // Источник - explain.php скрипта arms.zabbix (отвечает
    // по одному узлу за доли секунды, в Zabbix не ходит, ничего не
    // пишет). Имена наборов/правил берутся из rules.priv.php скрипта,
    // если там заданы. Ключ обязан быть 'zabbix-sync' (путь view).
    'zabbix-sync' => [
        'class' => \app\components\integrations\providers\ZabbixSyncProvider::class,
        'url' => 'https://synchost/arms.zabbix/explain.php',
        'token' => '<$explainToken из config.priv.php скрипта arms.zabbix>',
        //'title' => 'Постановка на мониторинг',
        //'verifySsl' => true, //по умолчанию сертификат не проверяется
        //'cacheTtl' => 0, //0 = обновлять при каждом открытии карточки
        //'timeout' => 5,
        //'zabbix' => 'zabbix', //id провайдера Zabbix для встраивания,
        //  если он назван иначе (само встраивание включается флагом
        //  'embedded' => true в конфиге провайдера Zabbix, см. выше)
    ],

    // «Порт коммутатора»: где адрес объекта виден в сети (коммутатор,
    // VLAN, порт). Открывается по клику иконки поиска рядом с MAC-адресом
    // (в карточке сама не рисуется: опрос занимает секунды и дёргает
    // сетевое оборудование). Опрашивает коммутаторы сервис
    // arms.macsearch, поставленный на машину с доступом к сетевому
    // оборудованию (у сервера инвентаризации такого доступа обычно нет),
    // но СОСТАВ опроса собирает сама инвентаризация: оборудование с типом
    // модели «Коммутатор», заполненным IP и неархивным состоянием, по
    // площадке объекта. Поэтому в выдаче коммутаторы - ссылками на свои
    // карточки, а порты, связанные с другими коммутаторами (по «Сетевым
    // портам»), помечаются как транзитные.
    // В карточке самого коммутатора появляется вторая панель - «Что
    // подключено к портам»: по кнопке снимается таблица MAC целиком и
    // раскладывается по портам (что за портом - ссылкой на объект).
    // Ключ обязан быть 'macsearch' (путь view-файлов).
    'macsearch' => [
        'class' => \app\components\integrations\providers\MacSearchProvider::class,
        'url' => 'http://macsearch.local:8088', //база сервиса
        'token' => '<токен из config.priv.json сервиса arms.macsearch>',
        //'title' => 'Порт коммутатора',
        //'autoPanel' => true, //рисовать панель в карточке автоматически,
        //  как у остальных интеграций (по умолчанию только по клику иконки)
        //'scope' => 'place', //область опроса ПО УМОЛЧАНИЮ: 'place' - площадка,
        //  где стоит объект, 'all' - все коммутаторы. В меню у адреса есть оба
        //  пункта, так что это лишь то, что предлагается первым
        //'switchTypes' => ['net_switch'], //коды типов оборудования (TechTypes.code),
        //  которые считаем коммутаторами; напр. ['net_switch','net_router']
        //'transitFrom' => 4, //с какого числа адресов на порту считать, что за
        //  ним сеть, а не устройство (карточка коммутатора, «Что подключено к
        //  портам»). Два-три адреса - штатное дело: телефон с ПК за ним,
        //  виртуалки. Связи «Сетевых портов» эту оценку перебивают
        //'bridgeToSwitchPorts' => ['internet','lan','switch','sw'], //как на
        //  устройствах-мостах (телефон с ПК за ним) подписан порт к коммутатору:
        //  подсказка, какой порт предложить первым в цепочке «порт → телефон →
        //  ПК»; в строке предложения он переключается
        //'bridgeToDevicePorts' => ['pc','comp'], //...и порт к устройству за мостом
        //'maxTargets' => 200, //предел коммутаторов в одном опросе
        //'maxMacs' => 3, //сколько адресов объекта искать
        //'includeLinked' => false, //не брать адреса привязанной ОС/АРМ (по умолчанию берутся)
        //'wait' => 25, //сколько сервис держит запрос, прежде чем ответить «идёт»
        //'timeout' => 30, //таймаут HTTP, обязан быть больше wait
        //'cacheTtl' => 600, //ttl панели; у сервиса свой кэш такой же длины
        //'maxAttempts' => 3, //сколько раз панель перезапросит себя, пока идёт опрос
        //'verifySsl' => true, //по умолчанию сертификат не проверяется
    ],

],

Обновление со старой схемы ad-reset. Раньше сброс пароля был отдельным провайдером ad-reset; теперь это действие провайдера ad. При обновлении: убрать из params-local.php ключ ad-reset (его опции defaultLength/smsText переносятся в ad), перезапустить php yii rbac/init (право действия теперь называется edit-integration-ad-reset-password) и перевыдать его ролям, которым сброс был выдан точечно; у ролей с глобальным edit ничего не меняется. Старые записи журнала остаются под провайдером ad-reset.

HttpTemplateProvider — универсальный провайдер для простых случаев «одна панель по одному GET-запросу с JSON-ответом»: применимость, запрос и рендер задаются целиком конфигом, писать PHP-класс не нужно. Подробности опций — в docblock класса.

Журнал действий

Каждое действие (и успешное, и неудачное) пишется в журнал: кто, когда, над каким объектом, с каким результатом. Шаги составных действий (сброс пароля → SMS) связываются ссылкой на инициатора. Секреты (пароли, тексты SMS) в журнал не попадают.

Просмотр — в админ-меню (иконка «шестерёнка») → Журнал интеграций (/integrations-log): список с фильтрами по интеграции, действию, результату, дате и инициатору; по клику — карточка записи со связанными шагами составного действия. Журнал только читается (создание/правка/ удаление недоступны).

Результат записи: ok — выполнено, error — не выполнено, run — начато, но не завершено (например, действие прервалось на середине).

Ограничения и заметки

  • Внешние ИС должны быть доступны с сервера инвентаризации (запросы выполняет backend, браузер пользователя во внешние ИС не ходит).
  • Кэш панелей общий на инстанс: два объекта с одной привязкой делят кэш, панель выглядит одинаково для всех пользователей. Кнопки действий в панели (сброс пароля) поэтому тоже видны всем, кто видит панель — доступ к самому действию сервер проверяет при открытии формы (пользователь без права получит «доступ запрещён»).

Диагностика из консоли

Панели ходят во внешние ИС с сервера, поэтому проблемы удобнее ловить там же (юнит-тесты в сеть не ходят):

php yii ldap/ping
php yii zabbix/ping
  • ldap/ping, ldap/account <login>, ldap/computer <имя>, ldap/auth <login> <пароль>, ldap/can-reset <кому> <исполнитель> <пароль> — доступность контроллера домена, данные учётки, путь в дереве и группы компьютера, проверка кредов и права на сброс (в AD ничего не пишет).
  • zabbix/ping — доступность API и валидность токена; zabbix/find <имя> — найти hostid узла; zabbix/host <hostid> — узел и его проблемы; zabbix/items <hostid> — item’ы метрик (распознан ли ключ, есть ли свежее значение); zabbix/object <класс> <id> (например zabbix/object comps 42) — сквозная проверка: применимость, привязка из external_links и то, что реально покажет панель в карточке.