Entwicklung von Univention Directory Listener-Modulen

From Univention Wiki

Jump to: navigation, search

Einführung

Der Univention Directory Listener ist Bestandteil der UCS-Domänenreplikation. Er erhält von den Notifier-Systemen der Domäne Informationen über geänderte LDAP-Objekte und kann anschließend die Objekte im LDAP des jeweiligen Notifier-Systems abrufen. Durch eine interne Datenbank kann zusätzlich auf vorherige Werte des jeweiligen Objektes zugegriffen werden.

Der Univention Directory Listener kann über eine Plugin-Architektur erweitert werden, so dass bei Änderungen im LDAP-Verzeichnisdienst lokale Aktionen ausgeführt werden können, zum Beispiel das Neuschreiben von Konfigurationsdateien, Zwischenspeicherung globaler LDAP-Einstellungen in lokalen Variablen oder Neustarts von Diensten.

Module für die Plugin-Architektur werden in Python implementiert.

Jedes Listener-Modul kann durch einen LDAP-Filter festlegen auf welche LDAP-Änderungen es angewendet werden soll. Es können auch mehrere Module auf eine Änderung angewendet werden.

Die Module werden unterhalb des Verzeichnisses /usr/lib/univention-directory-listener/system/ gespeichert. Beim Starten lädt der Listener-Dienst alle Module aus diesem Verzeichnis.

Alle Module legen nach ihrer Initialisierung so genannte Handler-Dateien im Verzeichnis /var/lib/univention-directory-listener/handlers/ ab. Diese geben den Status ihres Moduls in einem Zahlenwert an, eine 3 bedeutet beispielsweise, dass das Modul erfolgreich initialisiert wurde. Ist die Handler-Datei beim Start des Listeners nicht vorhanden, wird das Modul neu initialisiert. Bei der Initialisierung des Moduls wird das LDAP des Notifier-Systems komplett mit dem definierten Filter des Moduls durchsucht und für jedes gefundene LDAP-Objekt das Listener-Modul aufgerufen.

Aufbau eines Listener-Moduls

Header-Informationen

Die folgenden Attribute müssen für jedes Listener-Modul gesetzt werden:

Variable Erklärung
name Der eindeutige Name des Moduls. Er sollte am besten mit dem Namen der Listener-Moduldatei (ohne Erweiterung .py) übereinstimmen. Anhand dieses Bezeichners wird das Modul eindeutig identifiziert und er wird u.a. für die Zustandsdatei unter /var/lib/univention-directory-listener/handlers/ verwendet.
description Eine kurze Beschreibung des Moduls, die bisher nur intern bei der Fehlersuche verwendet wird.
filter Definiert den LDAP-Filter, bei dessen Übereinstimmung das Modul verwendet wird. Es ist zu beachten, dass dieser Filter - im Gegensatz zu ldapsearch - case-sensitive ist und in Klammern gesetzt werden muss.
Achtung: Der Name dieser Variable überdeckt unglücklicherweise die eingebaute Python-Funktion filter().
filters undocumented
attributes Eine Liste von LDAP-Attributnamen bei deren Änderung das Modul aufgerufen wird. Ist die Liste leer, wir das Modul bei Änderungen an jedem Attribut gestartet.
modrdn undocumented Bug #23324

Beispiel:

name = 'modul_name'
description = 'Ihre Beschreibung'
filter = '(objectClass=posixAccount)'
attributes = ['uid', 'uidNumber', 'cn']

Funktionen der Listener-Klasse

Mit

import listener

wird eine Python-Klasse des Univention Directory Listener eingebunden, die den Zugriff auf interne Listener-Informationen und die Univention Configuration Registry ermöglicht. Über das Python-Dictionary listener.baseConfig['<Variable>'] kann lesend auf alle UCR-Variablen zugegriffen werden.

Listener-Module arbeiten standardmässig mit den Rechten des Systembenutzers "listener". Für einige Operationen - etwa Dateioperationen in Systemverzeichnissen oder Neustarts von Diensten - kann es nötig sein das Modul kurzzeitig mit root-Rechten zu betreiben.

Wenn die listener-Klasse importiert wurde, kann dies durch listener.setuid(0) bzw. listener.unsetuid() durchgeführt werden:

  • listener.setuid(<uid>): Mit dieser Funktion kann das Modul mit einer abweichenden UserID betrieben werden, z.B. 0 für den Nutzer root.
  • listener.unsetuid(): Setzt die UserID wieder zurück

Mit der run-Funktion können Dienste oder Programme aus dem Modul heraus gestartet werden:

listener.run(<Programm>, <Argumente>, <uid>, <Warten>)
  • Programm: Gibt den vollständigen Pfad zum zu startende Programm oder Init-Skript an.
  • Argumente: Eine Liste von Argumenten, die dem zu startenden Programm übergeben werden. Da diese Liste intern an execv() übergeben wird, muß das erste Argument der Liste der Programmname sein.
  • uid: Die UserID unter der das Programm gestartet werden soll. Wird es nicht übergeben, wird das Programm mit der UID von root gestartet.
  • Warten: Gibt als Booleschen Wert (True, False) an, ob das Modul warten soll bis das auszuführende Programm terminiert ist oder der Ablauf sofort fortgesetzt wird. Standard ist Warten.


Funktionen der Debug-Klasse

Es besteht die Möglichkeit über die Klasse univention.debug zusätzliche Debug-Ausgaben zu erzeugen, die ebenfalls in der listener.log erscheinen:

univention.debug.debug(<id>, <level>, <string>)
  • id: Beschreibt den Dienst in dessen Log-Datei die Ausgaben erscheinen sollen, durch univention.debug.LISTENER in /var/log/univention/listener.log
  • level: Gibt den zu verwendenden Debug-Level an, zur Auswahl stehen:
    • ALL: wird erst ab Log-Level 4 ausgegeben.
    • INFO: Für grundsätzliche Ausgaben, wird erst ab Log-Level 3 ausgegeben.
    • PROCESS: wird erst ab Log-Level 2 ausgegeben.
    • WARN: Warnmeldungen, werden ab Log-Level 1 ausgegeben.
    • ERROR: Fehlermeldungen, werden immer in die Log-Datei und zusätzlich per syslog geschrieben.
  • string: Der auszugebende Text als Zeichenkette.

Funktionen des Moduls

Die folgenden Funktionen können für ein Listener-Modul definiert werden:

 def handler(dn, new, old)
 def handler(dn, new, old, command) # nur bei modrdn=True

Diese Funktion wird aufgerufen wenn der Filter zutrifft. Wird nur new übergeben, wurde das Objekt neu angelegt. Wird nur old übergeben, wird es gelöscht. Werden new und old übergeben, wird das zu behandelnde Objekt verändert.

dn
Der Distinguished Name des LDAP-Objekts als Zeichenkette, das verändert wurde.
old
None oder ein dict, das den gecachten Zustand des Objekts vor der Änderung entspricht.
new
None oder ein dict, das dem Zustand des Objekts nach der Änderung entspricht.
command
undocumented Bug #23324

old und new bilden jeweils die LDAP-Attributnamen auf ein Array von Zeichenketten ab.

def postrun()

Diese Funktion wird fünfzehn Sekunden nach der letzten Änderung aufgerufen, um z.B. Dienste neu zu starten. (Beachte, daß ein stetiger Strom von Änderungen ggf. dazu führt, daß diese Funktion erst sehr viel später ausgeführt wird!)

def prerun()

Diese Funktion wird einmalig vor mehreren Aufrufen von handler() ausgeführt. Der nächste Aufruf erfolgt erst wieder, nachdem postrun() ausgeführt wurde.

def initialize()

Diese Funktion wird einmalig bei der Initialisierung eines Moduls aufgerufen bzw. nach einem Resync. (Der Aufruf wird in der Datei /var/lib/univention-directory-listener/handlers/$MODULNAME protokolliert)

def clean()

Diese Funktion wird bei einem Resync des Modules vor dem nachfolgenden Aufruf von initialize() aufgerufen.

def setdata(key, value)

undocumented Bug #23324

basedn
binddn
bindpw
ldapserver

Neu-Initialisierung eines Listener-Moduls

Listener-Module werden beim Start des Univention Directory Listeners nicht neu initialisiert. Mit dem folgenden Befehl kann ein Modul neu initialisiert werden:

univention-directory-listener-ctrl resync <Modulname>

Der Listener wird dabei angehalten (dies kann ein paar Sekunden dauern), die Handlers-Datei des Moduls wird gelöscht und anschließend das Modul neu geladen.

Wenn alle Listener-Module neu initialisiert werden sollen, kann dies durch Stoppen des Listener, Löschen der bestehenden Handler-Dateien unterhalb von /var/lib/univention-directory-listener/handlers/ und anschließendem Start des Univention Directory Listeners geschehen.

Log-Dateien des Univention Directory Listener

In /var/log/univention/listener.log sind Debug-Ausgaben des Moduls zu finden. Detailliertere Ausgaben können durch Erhöhen des Loglevels in Univention Configuration Registry erzielt werden:

univention-config-registry set listener/debug/level=4

Der Log-Level sollte nach erfolgter Analyse zeitnah zurückgesetzt werden, da sonst die Log-Datei sehr schnell anwachsen kann.

Beispiel-Modul: PrintUsers

Das folgende Beispiel-Modul reagiert auf Änderungen an Nutzer-Objekten und speichert diese in der Datei /tmp/UserList.txt:

name='printusers'
description='print all names/users/uidNumbers into a file'
filter='''\
(|(&(objectClass=posixAccount)
    (objectClass=shadowAccount))
  (objectClass=univentionMail)
  (objectClass=sambaSamAccount)
  (&(objectClass=person)
    (objectClass=organizationalPerson)
    (objectClass=inetOrgPerson)))'''
attributes=['uid', 'uidNumber', 'cn']

import listener
import os
import sys
import univention.debug
import univention.utf8
import types

file='/tmp/UserList.txt'

def handler(dn, new, old):
  """ Funktion um in eine Textdatei zu schreiben, mit setuid(0)
  da der Handler als root ausgefuehrt wird: """
  def writeit(cn, uid, uidnr, com):
    """ Haengt CommonName, UserIDentifier, UID-Nummer und
    Kommentar an Datei an. """
    listener.setuid(0)
    try:
      o=open(file,'a')
      try:
        if com and com != 'indent':
          o.write('Name: "%s"\t\tUser: "%s"\t\tUID: "%s"\t%s\n'%
                  (univention.utf8.decode(cn), uid, uidnr, com))
        elif com == 'indent':
          o.write('\tName: "%s"\t\tUser: "%s"\t\tUID: "%s"\n'%
                  (univention.utf8.decode(cn), uid, uidnr))
        else:
          o.write('Name: "%s"\t\tUser: "%s"\t\tUID: "%s"\n'%
                  (univention.utf8.decode(cn), uid, uidnr))
      except Exception, e:
        univention.debug.debug(univention.debug.LISTENER,
               univention.debug.ERROR,
               'Failed to write to file "%s": %s'%(file,str(e)))
      o.close()
    except Exception, e:
      univention.debug.debug(univention.debug.LISTENER,
               univention.debug.ERROR,
               'Failed to open "%s": %s'%(file,str(e)))
      listener.unsetuid()

  # ----- Wenn ein Objekt veraendert wird:
  if new and old:
    # Schliesst Computerobjekte aus:
    if not new['uid'][0][-1:] == '$' and not old['uid'][0][-1:] == '$':
      univention.debug.debug(univention.debug.LISTENER,
        univention.debug.INFO, 'Edited User "%s"'%old['uid'][0])
      if old['uid'][0] == 'root' or old['uid'][0] == 'spam':
        writeit(old['cn'][0], old['uid'][0], '****', 'edited. \
          Is now:')
        writeit(new['cn'][0], new['uid'][0], '****', 'indent')
      else:
        writeit(old['cn'][0], old['uid'][0], old['uidNumber'][0],
          'edited. Is now:')
        writeit(new['cn'][0], new['uid'][0], new['uidNumber'][0],
          'indent')

  # ----- Wenn ein neues Objekt erzeugt wird:
  #	(Nach einer Initialisierung sind alle Benutzer "neu"!)
  if new and not old:
    if not new['uid'][0][-1:] == '$':
      univention.debug.debug(univention.debug.LISTENER,
        univention.debug.INFO, 'Added User "%s"'%new['uid'][0])
      if new['uid'][0] == 'root' or new['uid'][0] == 'spam':
        writeit(new['cn'][0], new['uid'][0], '****', 'added.')
      else:
        writeit(new['cn'][0], new['uid'][0], new['uidNumber'][0], 'added')

  # ----- Wenn ein Objekt geloescht wird:
  if old and not new:
    if not old['uid'][0][-1:] == '$':
      univention.debug.debug(univention.debug.LISTENER,
         univention.debug.INFO, 'Removed User "%s"'%old['uid'][0])
    if old['uid'][0] == 'root' or old['uid'][0] == 'spam':
      writeit(old['cn'][0], old['uid'][0], '****', 'removed')
    else:
      writeit(old['cn'][0], old['uid'][0], old['uidNumber'][0], 'removed')

def initialize():
  """ Initialisierungsfunktion, die Output-Datei wird
  geloescht, falls vorhanden. """
  if os.path.exists(file):
    try:
      listener.setuid(0)
      os.remove(file)
      listener.unsetuid()
      univention.debug.debug(univention.debug.LISTENER,
        univention.debug.INFO, 'Successfully deleted: "%s"'%file)
    except Exception, e:
      univention.debug.debug(univention.debug.LISTENER,
        univention.debug.WARN,
        'Failed to delete file: "%s": %s'%(file,str(e)))
  else:
    univention.debug.debug(univention.debug.LISTENER,
      univention.debug.INFO,
      'File: "%s" does not exist, going to create it!'%file)
Personal tools