GroupSync
Auf der MSXFAQ finden Sie einige Skripte und Beschreibungen zum Thema Verzeichnisabgleich, d.h. Um z.B. die Benutzer auf einem LDAP-Server als Kontakte im anderen LDAP-Server anzulegen. Solche Lösungen kommen immer wieder bei meinen Kunden zum Einsatz um z.B. Liste der gültigen Mailadressen auf einen anderen LDAP-Server beim Spamschutz zu übertragen oder bei Migrationen die GAL abzugleichen. Diese Skripte haben bislang das Thema Gruppen aber meist ausgespart und haben maximal Verteilergruppen in einer Exchange Organisation als Kontakte in der entfernten Organisation angelegt.
Verteiler kopieren und abgleichen
In Verbindung mit Exchange greift dies aber zu kurz, weil Gruppen mit Exchange Eigenschaften auch Kontakte als Mitglieder haben können, die z.B. von ADMT nicht übertragen werden. Hinzu kommt, dass grade beim Betrieb zweier Mailsysteme nebeneinander die Verteiler "besonders" zu behandeln sind. Bei einer Koexistenz sind Benutzer auf dem einen System in der Regel Kontakte auf der anderen Seite. Ein Verteiler kann dabei nun auf drei Arten verwaltet werden:
Variante 1: Ein Verteiler mit lokalen Usern und Kontakten und einen entfernten Kontakt
|
|
Variante 2: Ein Verteiler mit lokalen Usern und einem Kontakt zum einen entfernten Verteiler mit dortigen Benutzern
|
|
Variante 3: Zwei Verteiler mit synchronisierten Mitgliedern.
|
|
Es ist also nicht immer ausreichend oder optimal, wenn man eine Gruppe auf einer Seite auf der anderen Seite als Kontakt anlegt. Auch bei der Migration mit ADMT können zwar Gruppen übertragen werden, aber das ist auch immer nur ein "Additiver Merge" und kein Sync, d.h. Objekte die in der Quelle nicht mehr in einer Gruppen enthalten sind, werden im Ziel nicht entfernt und das Ziel muss ein AD sein.
Anforderung
Daher habe ich mir Gedanken gemacht, wie ich Gruppen in zwei AD-Forests ein eine Richtung abgleichen kann.
- Matching
Windows speichert die Mitglieder von Gruppen als DN im Feld "Member". Dieser String kann natürlich nicht 1:1 übertragen werden, da auf der anderen Seite der LDAP-Pfad sicher anders ist. Denkbar ist z.B. den SAMAccountName, wenn dieser gleich ist. Wenn es primär um Mailverteiler geht, dann kann die Mailadresse ein gutes Kriterium sein, sofern alle Objekte damit ausgestattet sind. Da ich PowerShell einsetze, kann ich das Script natürlich auch schnell anpassen. - Große Listen
Weiterhin muss berücksichtigt werden, dass LDAP-Server sehr große Mitgliederlisten nicht mit einem Aufruf ausliefern. Windows 2000 hat maximal 1000 Mitglieder geliefert. Windows 2003 liefert 1500 Mitglieder aus. Alles drüber muss mit einem "RangeSearch" abgeholt werden. - Merge und Sync
Es geht aber nicht nur darum die Mitglieder auf einer Seite zu exportieren und auf der anderen Seite zu importieren sondern zumindest optional sollte es auch möglich sein die Mitglieder im Ziel zu entfernen, wenn diese auch in der Quelle entfernt wurden. - Missing
Es kann natürlich auch sein, dass ein Objekte im Ziel noch gar nicht vorhanden ist und daher nicht addiert werden kann oder dass das Objekt im Ziel nicht eindeutig identifiziert werden kann. Es ist durchaus möglich, dass in einem Forest mit mehreren Domänen ein SamAccountName mehrfach vorhanden ist (z.B. Administrator). Hier sollte eine Warnung erfolgen. - Transport
Nicht immer sind Quelle und Ziel direkt per TCP miteinander verbunden. Daher habe ich mir überlegt den Export und den Import-Prozess einfach in zwei Skripte auszulagern, so dass eine Datei als Transfermedium genutzt werden kann. Zudem kann so auch ein Debugging einfach erfolgen. Man hat ja die Zwischendatei.
Funktionsweise
Klar kann ich ein PowerShell-Skript immer wieder anpassen aber am Ende läuft das auf viele unterversionen hinaus. Daher habe ich die Lösung in zwei Skripte aufgeteilt. Das "Export"-Skript hat nur die Aufgabe aus einer Quelle eine Liste der Mitglieder zu generieren. Mein Skript liest einen beliebige Gruppe in einem LDAP-Server aus. Die Liste können Sie aber auch aus einer anderen Quelle generieren, solange die Werte später im Ziel genutzt werden können.
Die rot umrandeten Blöcke sind auf dieser Seite
beschrieben.
Die Liste der Mitglieder wird dann an Import-GroupMember.ps1 per Pipeline übergeben, der die angegebene Gruppe im Ziel entsprechend aktualisiert. Sie können also den Export und den Import direkt aneinander ketten oder über die Pipeline die Zwischendaten in einer Datei ablegen und im Ziel wieder einlesen. Das Format der Datei ist recht einfach: einfach
Export
Schritt 1 ist der Export der Mitglieder einer gegebenen Gruppe mit einem vorgewählten Feld per PowerShell einfach in die Pipeline. So stehen mir alle Wege offen die Ausgabe in eine CSV,TXT,XML-Datei oder direkt an einen nachfolgenden Prozess zu übergeben. Die Grundfunktionen sind':
- Auslesen der Mitglieder einer als DN bekannten Gruppe aus eine, LDAP-Server
- Ermitteln des Exportfelds
pro Mitglied
Einen DN braucht ich nicht in ein anderes Ziel zu übertragen. Das Feld "SamAccountName", "Mail" oder "UserPrincipalName" sind vielleicht besser geeignet, um einen primären Schlüssel für das Ziel zu exportieren. - Keine Rekursion
Ich habe vereinfacht darauf verzichtet, Gruppen in Gruppen weiter aufzuschlüsseln. Die Gruppe als Mitglied wird natürlich auch mit exportiert und wenn Sie die zweite Gruppe ebenfalls mit GroupSync abgleichen, dann erhalten Sie auch eine vergleichbare Kopie. Wer mag, kann die Rekursion ja selbst schreiben. - Anmeldedaten für Quelle
Nicht immer ist die "integrierte Anmeldung mit dem aktuellen Benutzer der richtige Weg. Das Skript muss sowohl füre die Suche als auch die Pflege die Angabe von Anmeldedaten erlauben. - Ausgabe per Pipeline
Die PowerShell Pipeline ist sehr effektiv für die Verkettung von Skripten. Und wenn Sie keine direkte Kette nutzen können, weil Quelle und Ziel vielleicht gar keine direkte Verbindung haben, dann können Sie mit Out-File und Get-Content immer noch eine Datei als Transport nutzen. - Nutzung von ADSI ohne Add-ons
Ich weiß auch, dass Windows 2008 sehr leistungsfähige Commandlets zu AD-Gruppen hat. Aber diese erfordern einen Windows 2008R2 DC mit AD-WebServices und helfen nicht, wenn man andere LDAP-Server oder Quellen nutzen will. Daher habe ich "native" ADSI genutzt, auch wenn das etwas aufwändiger ist. - Logging als Transcript
Ein einfaches Logging der Aktionen sollte enthalten sein. mögliche Fehlerfälle sind neben Programmfehlern und falschen Anmeldedaten und nicht erreichbare Server auch Gruppe nicht da oder Mitglied ohne Export-Property.
Ich habe absichtlich den Export "einfach" gehalten und auf eine Gruppe beschränkt, die per Parameter anzugeben ist. Wenn ich mehrere Gruppen exportieren und woanders importieren will, dann kann ich das Script ja mehrfach aufrufen. Ich betrachte dieses Skript als "Baustein" in einer Sammlung und nicht als fertige Verzeichnisabgleichlösung. Daher gibt es auch keine XML-Datei als Konfigurationsprofil o.ä. Die Parameter können Sie im Header des Skripts sehen. Hier ein Beispiel, welche gegen meine Test-VM funktioniert.
param ( $ldapserver = "LDAP://192.168.23.130/", $groupdn = "CN=syncgroup3,OU=groupsync,DC=msxfaq,DC=local", $ldapUser = "MSXFAQ\Administrator", $ldappass = "Password!", $ldapauthtype = "Secure", $property = "mail" )
Dieses Setup exportiert die Mitglieder der Gruppe "syncgroup1". Das relevante Feld ist "Mail", d.h. Objekte ohne Mailadresse werden gar nicht exportiert.
Import
Das etwas aufwändigere Gegenstück betrifft den Import einer Mitgliederliste in einem Ziel. Auch hier gibt es ein paar Vorbedingungen zu können-.
- Objekte im Ziel schon
vorhanden
Die Aufgabe von GroupSync ist nur der Abgleich der Mitgliedschaften aber kein Ersatz für einen DirSync, der auch Objekte anlegt und pflegt. Es ist daher erforderlich, dass im Ziel sowohl die zu pflegende Gruppe als auch die möglichen Mitglieder vorhanden sind. Wenn Sie einen anderen Verzeichnisabgleich nutzen, dann müssen Sie GroupSync nach dem Anlegen der Objekte starten. - Eingabe per Pipeline
Das Skript erwartet die Liste der Mitglieder über die Pipeline. Das kann der Output von Export-GroupMember sein oder eine Datei, die per "Get-Content | ..." an das Skript gesendet wird. - Anmeldedaten für Ziel
Nicht immer ist die "integrierte Anmeldung mit dem aktuellen Benutzer der richtige Weg. Das Skript muss sowohl für die Suche als auch die Pflege die Angabe von Anmeldedaten erlauben. - Vollständigkeit der Daten
Beim Import der Daten muss das Skript sicherstellen, dass die Eingabedaten komplett sind. Stellen Sie sich vor der Export würde mitten drin abgebrochen und eine partielle Datei würde dem Import vorgelegt. Mit der Option "removeorphaned" entfernt das Skript Mitglieder, die in der Quelle nicht mehr enthalten sind. Das wäre gefährlich. Das Skript erwartet, dass am Ende der Datei drei "###" Zeichen stehen, um die Vollständigkeit zu prüfen.Match und Merge
Das Script sucht Eintrag für Eintrag als LDAP-Suche im angegebenen System um den DN des Objekts zu finden und den Benutzer zu addieren, wenn er noch nicht bereits Mitglied ist. Dazu holt das Skript zuerst eine komplette Liste der Mitglieder, aus der dann die gefundenen Objekte gestrichen werden. - Orphaned
Über den Schalter "RemoveOrphaned" wird eine Logik aktiviert, die am Ende all die Objekte aus der Gruppe entfernt, die in der Streichliste noch enthalten sind, weil Sie in der Quelle nicht enthalten waren. - Logging als Transcript und
Summen-CSV.
Wie der Export soll das Skript einmal alle Ausgaben als "Transcript" mitloggen und diese Logs nach einigen Tagen entfernen und zudem eine CSV-Datei aller im Ziel vorgenommenen Änderungen mitschreiben. Verschiedene Fehler im Code, bei den Parametern oder der eigentlichen Funktion sollen protokolliert werden.
Das Skript wird wie der Export über Parameter gesteuert, die eigentlich selbst erklärend sind. ich habe hier als Beispiel die Parameter, um mich von einem PC mit dem DC zu verbinden, obwohl es keinen Trust o.ä. gibt. Es werden in dem Fall explizite Anmeldedaten angegeben:
param ( $ldapserver = "LDAP://192.168.23.130/", $groupdn = "CN=syncgroup4,OU=groupsync,DC=msxfaq,DC=local", $searchpath = "GC://192.168.23.130/dc=msxfaq,dc=local", $ldapUser = "MSXFAQ\Administrator", $ldappass = "Password!", $ldapauthtype = "Secure", $property = "mail", $resultfile = ".\groupsync.change.csv", $removeorphaned = $true )
Ich habe dieses Skript absichtlich mit der expliziten Angabe von Credentials programmiert, da bei solchen "Sync"-Aufgaben in der Regel die grenzen von Forests überschritten werden und Trust nicht als gegeben angesehen werden können.
Mehrere Gruppen
Das Skript exportiert immer genau eine Gruppe. Wenn Sie mehrere Gruppen exportieren wollen, dann müssen Sie für jede Gruppe das Skript als Export und Import starten. Wer mit PowerShell etwas umgehen kann, kann das natürlich einfach in einer Schleife machen. Es bietet sich an im Skript einfach die Zugangsdaten zu hinterlegen und als Parameter dann nur die veränderlichen Komponenten anzugeben
get-group | foreach { export-groupmember.ps1 -outfile ($_.windowsemailaddress+."txt") -groupdn $_.distinguishedname import-groupmember.ps1 -infile ($_.windowsemailaddress+."txt") -groupdn ("cn="+$_.samaccountmame+",dc=msxfaq,dc=de") }
Dies ist nur ein Beispiel und sollte auf ihre Belange angepasst werden. Das Skript braucht beim Import einen absoluten distinguishedName. Wenn man den nicht irgendwie aus der Quelle ableiten kann, dann können Sie mit einer CSV-Datei ein Mapping selbst programmieren. Ursprünglich wollte ich die SMTP-Adresse als Ziel nutzen aber nicht alle Umgebungen nutzen Exchange im Ziel und GroupSync ist ja nicht zwingend für Exchange von Vorteil. Es sollte einem Admin aber kein Problem bereiten, das Import-Skript derart zu erweitern, dass eine Mailadresse einer Gruppe übergeben werden kann.
Beispiel
Der Einsatz ist relativ einfach und eine kurze Verkettung mit den Defaults im Parameter ist einfach ein :
.\export-groupmember.ps1 -property samaccountname | ` .\import-groupmember.ps1 -property samaccountname
Natürlich müssen Sie bei sich entweder die Parameter im Skript ändern oder ihre Werte beim Aufruf addieren.
Logging
Automatisch per Taskplaner gestartet Scripts sollten zumindest ein Protokoll schreiben. Festplattenplatz ist zu günstig um auf eine qualifizierte Fehlersuche zu verzichten. Daher schreiben beide Skripte entsprechende Logs
- Transcript
Alle Bildschirmausgaben werden mit "Start-Transcript" in eine jeweils neue Datei geschrieben. Der Dateiname leitet sich aus dem Script und einem Zeitstempel ab.
Diese Detailinformationen müssen aber nicht für immer aufbewahrt werden. Daher enthalten beide Skripte auch eine Funktion, die alte Dateien nach dem Namensschema nach 5 Tagen wieder löscht. - CSV Datei (Nur für den
Import)
Weiterhin wird eine CSV-Datei fortgeschrieben, die alle Veränderungen und mit einem Zeitstempel protokolliert.
Ich könnte mit vorstellen, das Script später auch noch mit Eventlog-Meldungen zu versehen.
Download
Das Archiv enthält beide Skripte:
- export-groupmember.ps1
Exportiert die Mitglieder einer Gruppe in eine Datei - import-groupmember.ps1
Aktualisiert die Mitglieder einer Gruppe anhand der Informationen einer Datei
Dies sind beides nur die Basis-Skripte aber liefern alleine natürlich einen Verzeichnisabgleich o.ä. Sie sind schon selbst gefordert die Liste der zu synchronisierenden Gruppen zu erstellen und die Skripte über FOR-Schleifen o.ä. Und individuellen Dateien aufzurufen.
Offen
- Meldungen im Eventlog
Damit eine zentrale Überwachung möglich ist, könnte das Skript Warnungen und Fehler im Eventlog melden. - Error Handling
Aktuell ist das Skript so ausgelegt, dass es bei einem Fehler abbricht. für einen dauerhaften unbewachten Betrieb wäre ein Abfangen und Melden der Fehler mit einem kontrollierten Abbruch wünschenswert. - Konfiguration als
Parameterdatei (XML)
Das macht es einfacher mehrere Gruppen abzugleichen. Dann muss aber die Ausgabe natürlich direkt in einer anzugebenden Datei landen und eine Verkettung geht nicht mehr so einfach - Optimierung mit Cache-Datei
Wenn ich in einer großen Umgebung mit mehreren tausend Anwendern und Mitgliedern arbeite, dann dauert der Export der aktuellen Mitglieder in einen Hashtable nur kurz. Aber die zum addieren erforderliche Suche von Objekten ist sehr Ressourcenintensiv. Hier könnte man optimieren, indem man sich die Eingangdaten mit der vorherigen Version abgleich und so schon mal erkennt, wer neu dazu gekommen ist. Damit man dann aber auhc die löschen kann, die entfallen sind, muss man diese auch suchun und dann aus der Gruppen entfernen. Dann könnte man aber komplett auf einen Abgleich der "Member" verzichten. Allerdings ist dabei natürlich zwinged erforderlich, dass niemand anderes das Feld "member" pflegt. Ansonsten laufen die Mitglieder der Quelle und des Ziels auseinander.
Weitere Links
- MiniSync
- Verbinden von Organisationen
- Verzeichnisabgleich
- PS Pipeline
- PS Debugging
- PS Beispiele
- Using
System.DirectoryServices to
Search the Active Directory
http://msdn.microsoft.com/en-us/library/ms973834.aspx - Working with Active
Directory using PowerShell ADSI
adapter
http://social.technet.microsoft.com/wiki/contents/articles/4231.working-with-active-directory-using-PowerShell-adsi-adapter.aspx - Create Exact Copies of
Active Directory Security Groups
in a New Forest
http://blogs.technet.com/b/heyscriptingguy/archive/2010/10/17/create-exact-copies-of-active-directory-security-groups-in-a-new-forest.aspx - Using Range Search für >
1500 Members
http://adadmin.blogspot.de/2009/12/get-group-members-of-large-groups-via.html - http://mow001.blogspot.de/2006/04/large-ad-queries-in-monad.html
- http://www.adilhindistan.com/2013/01/getting-members-of-large-groups-via.html
- http://activelydirect.blogspot.de/2011/05/exporting-membership-of-large-nested.html
- Adding members to groups
with +1500 members in PowerShell
http://wouter.shush.com/2007/10/adding-members-to-groups-with-1500-members-in-PowerShell - List Members of Large Group
http://gallery.technet.microsoft.com/scriptcenter/List-Members-of-Large-Group-0eea0132