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

Сегодня хочу поделиться опытом того, как я отказался от стандартного пакета 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. А с какими неожиданными проблемами сталкивались вы, используя “системную шину данных”?

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