Привет, Хабр! Меня зовут Габидуллин Александр, я старший инженер в отделе архитектуры и интеграции. Занимаюсь внедрением продуктов компании, и автоматизацией рутинных задач.

Сегодня хочу поделиться опытом того, как я отказался от стандартного пакета sssd-tools для мониторинга службы SSSD в пользу прямого общения с демоном через D-Bus и создал свой первый Ansible-модуль.

Недавно я уже сделал небольшую утилиту для мониторинга различных служб через системный трей в Linux - исходник лежит тут.

И далее подумал об импортозамещении, что было бы удобно по нажатию одной кнопки в трее смотреть: информацию о домене, применять групповые политики, и если что-то сломалось подсказать админу информацию о своей машине или выполнить какие-то команды. Так я начал писать applet для ALD Pro (Freeipa тоже поддерживается, но с меньшим функционалом).

Первоначально я использовал утилиту sssctl из пакета sssd-tools, с помощью которой я мог взаимодействовать с доменом и службой SSSD, но это добавляло лишние зависимости для моего приложения.

Зависимости и почему захотелось от них избавиться

После обсуждения с коллегами, я понял, что не стоит усложнять пакет различными зависимостями, можно общаться с SSSD напрямую через D-Bus.

Прямое взаимодействие даёт:

  • Более быстрый отклик

  • Отсутствие промежуточных обработчиков

  • Минимальное количество зависимости

Мой pull-request в ansible.general

D-Bus как решение: зачем и почему

D-Bus (сокращенно от «Desktop Bus») — это система межпроцессного взаимодействия (IPC), которая позволяет приложениям на одном компьютере общаться друг с другом, обмениваясь сообщениями. Она действует как «шина сообщений», через которую один процесс может отправлять запросы, сигналы и данные другим процессам.

Теперь необходимо было понять, как с ним общаться?

На помощь приходит dbus-monitor — утилита для просмотра D-Bus сообщений в реальном времени.

1. Ищем интерфейс, связанный с нашей службой SSSD:

dbus-send --system --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.ListNames

2. Вывод, который мы получаем:

method return time=1764241336.897765 sender=org.freedesktop.DBus -> destination=:1.638205 serial=3 reply_serial=2
   array [
      string "org.freedesktop.DBus"
      string "org.freedesktop.systemd1"
      string "com.redhat.oddjob"
      string "org.freedesktop.ModemManager1"
      string "org.freedesktop.NetworkManager"
      string "org.freeipa.server"
      string "org.fedorahosted.certmonger"
      string "ru.astralinux.FileManager.SmbWatcher"
      string "org.freedesktop.PolicyKit1"
      string "ru.astralinux.events"
      string "com.redhat.idm.trust"
      string "org.freedesktop.UDisks2"
      string "fi.w1.wpa_supplicant1"
      string "org.freedesktop.login1"
      string "org.freedesktop.sssd.infopipe"
      string "com.redhat.oddjob_mkhomedir"
      string "org.freedesktop.UPower"
.......
   ]

3. Находим интересующий нас интерфейс:

string "org.freedesktop.sssd.infopipe"

4. Подключаемся и начинаем слушать сообщения от SSSD:

dbus-monitor --system "sender=org.freedesktop.sssd.infopipe" "destination=org.freedesktop.sssd.infopipe"

5. В другом терминале запускаем sssctl, и смотрим какие запросы и ответы получает наш интерфейс D-Bus:

sssctl domain-status ald.pro

6. В выводе видим:

method call time=1764242184.579534 sender=:1.638429 -> destination=org.freedesktop.sssd.infopipe serial=2 path=/org/freedesktop/sssd/infopipe/Domains/ald_2epro; interface=org.freedesktop.sssd.infopipe.Domains.Domain; member=IsOnline
method call time=1764242184.579878 sender=:1.103398 -> destination=org.freedesktop.DBus serial=60344 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=GetConnectionUnixUser
   string ":1.638429"
method return time=1764242184.579898 sender=org.freedesktop.DBus -> destination=:1.103398 serial=1079864 reply_serial=60344
   uint32 0
method return time=1764242184.580558 sender=:1.103398 -> destination=:1.638429 serial=60345 reply_serial=2
   boolean true
method call time=1764242184.580681 sender=:1.638429 -> destination=org.freedesktop.sssd.infopipe serial=3 path=/org/freedesktop/sssd/infopipe/Domains/ald_2epro; interface=org.freedesktop.sssd.infopipe.Domains.Domain; member=ListServices
method return time=1764242184.581291 sender=:1.103398 -> destination=:1.638429 serial=60346 reply_serial=3
   array [
      string "IPA"
   ]
method call time=1764242184.581431 sender=:1.638429 -> destination=org.freedesktop.sssd.infopipe serial=4 path=/org/freedesktop/sssd/infopipe/Domains/ald_2epro; interface=org.freedesktop.sssd.infopipe.Domains.Domain; member=ActiveServer
   string "IPA"
method return time=1764242184.583142 sender=:1.103398 -> destination=:1.638429 serial=60347 reply_serial=4
   string "aldprodc1.ald.pro"
   ]

Это показывает нам:

  • Вызов метода IsOnline для домена ald.pro (закодированного как ald_2epro)

  • Ответ boolean true — домен онлайн

Именно наблюдение за реальными вызовами помогло понять структуру общения с D-Bus, что помогло заменить команды с использованием sssctl на общение через D-Bus.

Приведу пару примеров кода, который используется в приложение:

def get_dbus_iface():
    try:
        domain = get_domain_name()
        domain = domain.replace(".", "_2e")
        bus = dbus.SystemBus()
        sssd_object = bus.get_object('org.freedesktop.sssd.infopipe', f"/org/freedesktop/sssd/infopipe/Domains/{domain}")
        iface = dbus.Interface(sssd_object, dbus_interface='org.freedesktop.sssd.infopipe.Domains.Domain')

        return iface

    except Exception as e:
        response = f"Ошибка при выполнении команды: {e}"

        return response

@method
async def domain_status():
    iface = get_dbus_iface()

    try:
        response = iface.IsOnline()
        response = str(response)

        return Success(response)

    except Exception as e:

        return Error(code=1, message=f"Ошибка при выполнении команды: {e}")

@method
async def get_active_dc():
    try:
        iface = get_dbus_iface()
        response = iface.ActiveServer('IPA')
        response = str(response)

        return Success(response)

    except Exception as e:
        response = f"Ошибка при выполнении команды: {e}"

        return Error(code=1, message=response)      

Как можно увидеть, структура D-Bus в части sssd выглядит так:

org.freedesktop.sssd.infopipe                                 # Сервис
└── /org/freedesktop/sssd/infopipe/Domains/                   # Объекты доменов
    └── /org/freedesktop/sssd/infopipe/Domains/example_2ecom  # Конкретный домен
        └── org.freedesktop.sssd.infopipe.Domains.Domain      # Интерфейс

Таким образом перебираем все возможные и нужные команды, и переписываем их на работу с D-Bus, а далее функции и идемпотентность кода.

Также стало удобно определять домен, к которому принадлежит машина, и взаимодействовать с ним напрямую через D-Bus или через другой код при необходимости.

Идея Ansible-модуля: от мониторинга к автоматизации

Идея модуля возникла на стыке двух задач: с одной стороны, это был факультативный проект для освоения возможностей Ansible, с другой — желание применить DevOps-практики, уйдя от модулей cmd и shell для работы с доменом через sssctl.

Поэтому необходимо было реализовать удобный, идемпотентный и среда-независимый модуль.

Ключевые особенности реализации

1. Кодирование доменных имен

Важно заметить, что символ . не используются для общения с D-Bus SSSD демона, он заменяется на _2e для корректного формирования D-Bus путей.

    def _get_domain_path(self, domain: str) -> str:
        """Convert domain name to D-Bus path format.
        Args:
            domain: Domain name to convert.
        Returns:
            D-Bus path for the domain.
        """
        return f'/org/freedesktop/sssd/infopipe/Domains/{domain.replace(".", "_2e")}'

2. Поддержка разных типов серверов

Так как домен не обязательно может быть на базе Linux, важно учесть возможность указания типов серверов. Таким образом можно использовать IPA или AD тип, в зависимости от необходимой информации о статусе домена.

    def get_active_servers(self, domain: str, server_type: str) -> dict:
        """Get active servers for domain.

        Args:
            domain: Domain name to get servers for.
            server_type: Type of servers ('IPA' or 'AD').

        Returns:
            Dictionary with server types as keys and server names as values.
        """
        domain_obj = self._get_domain_object(domain)
        iface = dbus.Interface(domain_obj, dbus_interface=self.DOMAIN_INTERFACE)

        if server_type == 'IPA':
            server_name = f'{server_type} Server'
            return {server_name: iface.ActiveServer(server_type)}
        elif server_type == 'AD':
            return {
                'AD Global Catalog': iface.ActiveServer(f'sd_gc_{domain}'),
                'AD Domain Controller': iface.ActiveServer(f'sd_{domain}')
            }
        return {}

3. Детальная обработка ошибок D-Bus

Разбираем D-Bus ошибки по именам для понятных сообщений.

    try:
        if action == 'domain_status':
            result['online'] = sssd.check_domain_status(domain)
        elif action == 'domain_list':
            result['domain_list'] = sssd.get_domain_list()
        elif action == 'active_servers':
            result['servers'] = sssd.get_active_servers(domain, server_type)
        elif action == 'list_servers':
            result['list_servers'] = sssd.list_servers(domain, server_type)

    except dbus.exceptions.DBusException as e:
        dbus_error_name = e.get_dbus_name()
        if dbus_error_name == 'org.freedesktop.DBus.Error.UnknownObject':
            module.fail_json(msg=f'Domain not found: {domain}', **result)
        elif dbus_error_name == 'org.freedesktop.DBus.Error.UnknownInterface':
            module.fail_json(msg=f'Interface not found for domain: {domain}', **result)
    # ... другие специфические ошибки

Использование в Ansible

После получения одобрения от сообщества можно будет использовать мой модуль из публичного хранилища community.general

Пример playbook:

- name: SSSD health check
  hosts: all
  tasks:
    - name: Get domain list
      sssd_info:
        action: domain_list
      register: domains

    - name: Check status for each domain
      sssd_info:
        action: domain_status
        domain: "{{ item }}"
      loop: "{{ domains.domain_list }}"
      register: domain_status

    - name: Alert if any domain is offline
      debug:
        msg: "Domain {{ item.item }} is offline!"
      loop: "{{ domain_status.results }}"
      when: item.online == 'offline'

Преимущества решения

Надежность:

  • Стабильный ответ вместо парсинга текста;

  • Детальная обработка ошибок;

  • Совместимость между версиями SSSD.

Функциональность:

  • Поддержка всех основных операций мониторинга;

  • Структурированный JSON вывод;

  • Интеграция с Ansible-экосистемой;

  • Идемпотентность модуля.

Заключение

В итоге я освоил работу с D-Bus и нашел способ замены системных утилит на прямое взаимодействие — это сразу облегчило приложение и дало заметный прирост производительности.

Такой подход оказался быстрее и надежнее, плюс — он идеально встраивается в автоматизацию. А для изучения шины данных незаменимым помощником стал dbus-monitor.

И также я надеюсь помочь сообществу свои модулем, который я отправил в ansible-community.

Вопрос для дочитавших данный материал

А как вы используете D-Bus в своих проектах?

Работа с D-Bus полна трудностей, в моем примере символ . не использовался для общения с SSSD демоном, он заменяется на _2e. А с какими неожиданными проблемами сталкивались вы, используя “системную шину данных”?

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


  1. mrkrivedko
    04.12.2025 08:57

    Коллега приветствую, у меня был как раз опыт взаимодействия с sssd через dbus. Важно отметить, что для infopipe потребуются привелегии.

    вот полезная ссылка по взаимодействию с sssd через dbus

    По поводу преобразованию . в _2e это происходит согласно спецификации DBus, где допустимый объектный путь должен выполнять правило: "Каждый элемент должен содержать только символы ASCII "[A-Z] [a-z] [0-9] "", поэтому в $domain если содержится символ он будет экранирован в <hex-код ascii>

    Собственно чтобы узнать список доменов на машине дергаем метод org.freedesktop.sssd.infopipe.ListDomains(),а далее можно работать с валидными доменами.

    Также dbus удобно смотреть через busctl :

    busctl tree org.freedesktop.sssd.infopipe

      

    busctl introspect org.freedesktop.sssd.infopipe /org/freedesktop/sssd/infopipe/Domains/$domain

    Для отслеживания dbus можно использовать gui утилиту qdbusviewer

    Она находится в пакете qttools5-dev-tools (для Астралинукса так называется)


    1. MikeyTide Автор
      04.12.2025 08:57

      Супер! Спасибо что подробно и по теме.

      Благодарю за информацию


  1. Vecna001
    04.12.2025 08:57

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

    Вы «избавились от зависимости от sssctl, но по факту:

    • отказались от публичного, поддерживаемого API,

    • и напрямую подключились к внутренней реализации демона, подсмотренной через dbus-monitor.

      Это не инженерное решение, это любительский реверс по живому production.

    Про «минимальное количество зависимостей» особенно показательно:

    • dbus-python в рантайме,

    • жёсткая привязка к infopipe,

    • зависимость от конкретной реализации SSSD.
      Зависимостей стало не меньше — они просто стали хуже по качеству.

    «Заметный прирост производительности» без единого замера — это отдельный жанр технической фантастики.
    sssctl ходит в тот же D-Bus. Вы ускорили ровно то же самое действие, убрав CLI-обёртку с временем работы меньше миллисекунды, но назвали это оптимизацией.

    Хардкод путей вида:

    /org/freedesktop/sssd/infopipe/Domains/{domain.replace(".", "_2e")}
    

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

    Ansible-модуль — отдельная категория:

    • строки вместо булевых значений,

    • отсутствует декларативность,

    • идемпотентность заявлена, но не реализована,

    • changed отсутствует как класс.
      Это не модуль автоматизации, это Python-скрипт с завышенной самооценкой.

    Обработка ошибок — формула «поймали всё и ничего не поняли».
    В эксплуатации это превращается в бесконечное «Произошла ошибка» без диагностической ценности.

    Использование dbus-monitor как основы интеграции — прямой индикатор уровня: диагностический сниффер выдается за спецификацию API. Контракты не описаны, версии не зафиксированы, документация отсутствует — зато присутствует слово «архитектура».

    И, конечно, «импортозамещение». Его здесь нет вообще. Это просто декоративный термин без технического содержания.

    Итог: статья демонстрирует, как:

    • заменить поддерживаемый интерфейс на внутренний,

    • потерять совместимость,

    • ничего не измерить,

    • назвать это «архитектурным улучшением»,

    • и отправить в Ansible как «модуль».

    Для sandbox — допустимо.
    Для production — классический пример создания технического долга в момент написания кода.


    1. MikeyTide Автор
      04.12.2025 08:57

      Приветствую! Очень подробно и по теме, но не со всем я готов согласиться.
      На то это и форум, где каждый говорит что думает, а мой код - это опенсорс, и любой может прийти и добавить комментарии в PR.

      Отказались от публичного, поддерживаемого API

      Я не отказался, а как вы правильно указали сделал углубленное изучение общения утилиты и системы.
      Далее просто убрал утилиту, и общаюсь самостоятельно, минуя proxy в лице sssctl

      Про «минимальное количество зависимостей» особенно показательно

      Dbus-python vs sssd-tools — это переход от функциональной зависимости к системной абстракции. D-Bus — это стандартный системный IPC-механизм, который уже присутствует в любом современном дистрибутиве. Это как заменить прямой вызов конкретной программы на использование системного сокета.

      Жёсткая привязка к infopipe

      Жёсткая привязка к infopipe — но это и есть официальный публичный D-Bus интерфейс SSSD, зарегистрированный в системе. Это не reverse engineering, а использование предоставленного разработчиками SSSD API, который более стабилен, чем парсинг CLI вывода.

      Зависимость от конкретной реализации SSSD

      Dbus-python является стандартной библиотекой для работы с системной шиной, тогда как sssd-tools — это специфичная утилита конкретного пакета. Первая является частью экосистемы системного программирования на Python, вторая — внешней CLI обёрткой.

      Заметный прирост производительности» без единого замера — это отдельный жанр технической фантастики

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

      Однако позвольте объяснить, почему даже без точных цифр этот подход даёт системные преимущества.

      Вы утверждаете, что sssctl тоже использует D-Bus, но здесь есть принципиальная разница: прямой вызов метода против запуска процесса, который внутри себя делает тот же вызов.

      Даже если накладные расходы на запуск процесса составляют миллисекунды, в контексте Ansible-модуля, выполняющегося на сотнях хостов, эта разница становится значимой.

      Но что ещё важнее — это качество интеграции. Для модуля Ansible, который должен быть предсказуемым и лёгким, прямой D-Bus вызов — это более чистая архитектура, даже если абсолютный прирост скорости измеряется миллисекундами на одном вызове.

      Хардкод путей вида: /org/freedesktop/sssd/infopipe/Domains/{domain.replace(".", "_2e")}

      Путь /org/freedesktop/sssd/infopipe/Domains/{domain} — это не хардкод в смысле "зашитых значений", а следование документально зафиксированной схеме D-Bus именования, которую использует сам SSSD.

      Кодирование точки как _2e — это стандартное преобразование D-Bus для специальных символов в object paths, а не произвольное решение.

      Важный нюанс: если разработчики SSSD изменят эту схему путей в будущей версии, то сломается не только мой код, но и любой другой софт, использующий этот интерфейс, включая потенциально GUI-утилиты и системные мониторинговые инструменты.
      D-Bus интерфейсы, особенно зарегистрированные в системе, имеют определённые ожидания стабильности.

      Ansible-модуль — отдельная категория

      Мой PR открыт для комментариев, на то это и open-source

      https://github.com/ansible-collections/community.general/pull/11120


  1. Tamerlan666
    04.12.2025 08:57

    По Ansible модулю. Сама идея хорошая и контрибьют приветствуется. Только пока что модуль еще на этапе код ревью и PR не принят. Я бы на вашем месте дождался хотя бы апрува и мержа в develop, если не ближайшего релиза, прежде чем рекламировать свои достижения. Тогда модулем могли бы сразу воспользоваться все желающие, узнавшие о нем из статьи. А сейчас это доступно лишь избранным энтузиастам, не поленившимся скачать сырой код из вашего форка ради экспериментов.

    Сам код модуля на вид довольно сырой. Отсутствуют integration тесты. Ну, и на данный момент, насколько я могу видеть, код не проходит даже первичные quality gates в виде линтеров вроде pep8. На вашем месте я бы освоил ansible-test и довел локально код до ума хотя бы в плане следования стандартам оформления. Чтобы вас не заворачивал на ревью тупой автолинтер по формальным признакам.


    1. MikeyTide Автор
      04.12.2025 08:57

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

      А линтеры поругались только на тесты в их ci/cd. А сам код уже все ок, там все поправлено и сделано.

      А с тестами сложно протестировать этот модуль в рамках ci/cd, так как опыт написания тестов у меня не большой и я попросил помощи коллег в ПР с тестами.


      1. Tamerlan666
        04.12.2025 08:57

        В CI там запускается простая конструкция ansible-test для sanity, которую вы легко бы запустили у себя локально, если бы потратили на это чуток времени.

        Тесты писать не всегда просто, но без них поддерживать модуль в дальнейшем будет сложно, т.к. нет гарантий что не поймаешь какую-то регрессию при обновлении каких-то зависимостей.


        1. MikeyTide Автор
          04.12.2025 08:57

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

          Конечно, буду разбираться с модулями и автотестами благодаря сообществу!

          Ну и вам спасибо, что хоть и критикуете, но говорите о правильных вещах и по делу.