Dans le cadre d'une migration d'annuaire, si on ne veut pas procéder au changement de produit en mode "big bang", il est généralement obligatoire de mettre en place un mécanisme de synchronisation entre l'ancienne et la nouvelle infrastructure.
Sachant que la réplication est propre à chaque produit, il est fort probable qu'il ne soit pas possible d'utiliser la réplication native des annuaires en tant que telle.
Il faut donc passer par une solution externe, soit fournie par l'éditeur lui-même (c'est le cas de DataSync chez Ping Identity, ou de ISDI chez IBM), soit fournie par un autre produit.
Nous allons étudier ici le fonctionnement de LSC Project lsc-project.org, qui permet ce genre de choses.
La documentation (https://lsc-project.org/documentation/latest/start) du produit a été utilisée pour réaliser ces tests.
LSC est écrit en Java, et utilise une JRE en version 1.6 au minimum. Avant d'installer, on s'assure que la variable JAVA_HOME est bien définie. Note : LSC n'est pas compatible avec Java 11. Il faut donc au maximum un Java 8:
yum install -y java-1.8.0-openjdk.x86_64
export JAVA_HOME=/usr/lib/jvm/jre-openjdk
echo $JAVA_HOME
/usr/lib/jvm/jre-openjdk
java -version
openjdk version "1.8.0_345"
OpenJDK Runtime Environment (build 1.8.0_345-b01)
OpenJDK 64-Bit Server VM (build 25.345-b01, mixed mode)
Notre environnement de test se compose donc de 3 serveurs (containers LXC) :
Les données ont été exportées depuis l'annuaire ODSEE :
/opt/dsee7/bin/dsadm export /path/to/{instance} {root suffix} /path/to/LDIF.file
Par exemple : /opt/dsee7/bin/dsadm export /var/lib/dsee/example dc=example,dc=com /opt/backup/example/Export-${TODAY}.ldif
Le serveur 389DS a été installé, une instance a été créée, avec le même root suffix que l'ancien annuaire. Les données ont ensuite été importées :
dsctl {instance} ldif2db {backend} /xchange/ldap/Export-20220919.ldif
Note : l'import initial n'est pas nécessaire, puisque la synchronisation permettrait de créer les entrées. Selon la volumétrie cependant, ceci peut être plus rapide. En faisant un import, on va également récupérer les attributs opérationnels d'origine (ce qui n'est pas le cas avec la synchronisation).
On aura donc les éléments suivants :
| Hostname | IP | Port | Role |
|---|---|---|---|
| odsee | 10.10.27.173 | 389 | Annuaire LDAP DSEE |
| rhds001 | 10.10.27.73 | 389 | Annuaire LDAP 389DS |
| lsc | 10.10.27.227 | Serveur de synchro LSC |
Note : on aurait pû également installer LSC sur l'un des serveurs d'annuaire.
On peut télécharger à partir de la page de téléchargement lsc-project.org/download.
Plusieurs options sont possibles : archive, package RPM ou DEB, repository. Dans notre exemple, nous allons utiliser l'archive.
# cd /opt
# wget https://lsc-project.org/archives/lsc-core-2.1.6-dist.tar.gz
# tar zxf lsc-core-2.1.6-dist.tar.gz
# ls lsc-2.1.6/
LICENSE.txt README.md bin doc etc lib sample
Les répertoires contiennent donc :
La configuration de LSC se trouve dans un fichier principal : etc/lsc.xml.
Comme stipulé dans la documentation :
Un fichier lsc.xml-sample est fourni avec l'archive. On peut donc commencer par le copier :
cp etc/lsc.xml-sample etc/lsc.xml
Le fichier comporte plusieurs sections :
Pour une connexion de type LDAP configuration/connections/ldap, on aura les informations suivantes :
Si on veut sychroniser les données avec un compte de l'annuaire, il faudra démarrer en ayant chargé le compte dans les données au début, ou démarrer avec le compte système (ex : cn=Directory manager), puis une fois la première sychro lancée, utiliser un compte ayant suffisamment de droits.
Exemple de définition pour les annuaires LDAP de notre exemple :
<connections>
<ldapConnection>
<name>ldap-odsee</name>
<url>ldap://odsee:1389/dc=example,dc=com</url>
<username>cn=Directory Manager</username>
<password>secret</password>
<authentication>SIMPLE</authentication>
<referral>IGNORE</referral>
<derefAliases>NEVER</derefAliases>
<version>VERSION_3</version>
<pageSize>-1</pageSize>
<factory>com.sun.jndi.ldap.LdapCtxFactory</factory>
<tlsActivated>false</tlsActivated>
</ldapConnection>
<ldapConnection>
<name>ldap-rhds</name>
<url>ldap://rhds001:389/dc=example,dc=com</url>
<username>cn=Directory Manager</username>
<password>Pa55word_4_DS</password>
<authentication>SIMPLE</authentication>
<referral>IGNORE</referral>
<derefAliases>NEVER</derefAliases>
<version>VERSION_3</version>
<pageSize>-1</pageSize>
<factory>com.sun.jndi.ldap.LdapCtxFactory</factory>
<tlsActivated>false</tlsActivated>
</ldapConnection>
</connections>
Les tâches définissent les actions à réaliser entre une source et une destination. Une tâche contient les paramètres suivants :
Pour chaque tâche on définit également :
Dans chaque service on va paramétrer le DN de base, les requêtes de recherche, la liste des attributs à récupérer et les conditions.
On peut donc filtrer sur les attributs que l'on veut comparer, et les DN de base.
On peut utiliser du script (JavaScript ou Groovy) dans différents endroits du traitement :
Il est donc possible de calculer des attributs de la cible en fonction d'attributs de la source. Par exemple, si on veut calculer un attribut displayName en concaténant le prénom et le nom en majuscules :
<dataset>
<name>displayname</name>
<policy>FORCE</policy>
<defaultValues>
</defaultValues>
<forceValues>
<string>js:srcBean.getDatasetFirstValueById("givenname") + " " + srcBean.getDatasetFirstValueById("sn").toUpperCase()
</string></forceValues>
<createValues></createValues>
</dataset>
Note : dans le cas de calcul, il faut également que l'attribut soit dans la liste des attributs récupérés (fetchedAttributes).
Les conditions déterminent quelles actions sont possibles, et sous quelle condition. Dans l'exemple suivant, on gère toujours les créations, on ne fait la mise à jour que si la source a été modifiée ultérieurement à la cible, et on traite pas les suppressions ou changements de UID :
<conditions>
<create>true</create>
<update>srcBean.getDatasetFirstValueById('modifyTimeStamp') > dstBean.getDatasetFirstValueById('modifyTimeStamp')</update>
<delete>false</delete>
<changeId>false</changeId>
</conditions>
Extrait de ma configuration d'exemple avec la tâche de sychronisation DSEE vers 389DS :
<tasks>
<task>
<name>odsee-people-2-rhds</name>
<bean>org.lsc.beans.SimpleBean</bean>
<ldapSourceService>
<name>dsee-src-service</name>
<connection reference="ldap-odsee" />
<baseDn>ou=People,dc=example,dc=com</baseDn>
<pivotAttributes>
<string>uid</string>
</pivotAttributes>
<fetchedAttributes>
<string>cn</string>
<string>givenname</string>
<string>sn</string>
<string>objectClass</string>
<string>uid</string>
<string>mail</string>
<string>employeenumber</string>
<string>telephonenumber</string>
<string>homephone</string>
<string>mobile</string>
<string>displayname</string>
<string>userPassword</string>
</fetchedAttributes>
<getAllFilter>(objectClass=inetorgperson)</getAllFilter>
<getOneFilter>(&(objectClass=inetorgperson)(uid={uid}))</getOneFilter>
<cleanFilter>(&(objectClass=inetorgperson)(uid={uid}))</cleanFilter>
</ldapSourceService>
<ldapDestinationService>
<name>ldap-rhds-service</name>
<connection reference="ldap-rhds" />
<baseDn>ou=People,dc=example,dc=com</baseDn>
<pivotAttributes>
<string>uid</string>
</pivotAttributes>
<fetchedAttributes>
<fetchedAttributes>
<string>cn</string>
<string>givenname</string>
<string>sn</string>
<string>objectClass</string>
<string>uid</string>
<string>mail</string>
<string>employeenumber</string>
<string>telephonenumber</string>
<string>homephone</string>
<string>mobile</string>
<string>displayname</string>
<string>userPassword</string>
</fetchedAttributes>
<getAllFilter>(objectClass=inetorgperson)</getAllFilter>
<getOneFilter>(&(objectClass=inetorgperson)(uid={uid}))</getOneFilter>
</ldapDestinationService>
<propertiesBasedSyncOptions>
<mainIdentifier>"uid=" + srcBean.getDatasetFirstValueById("uid") + ",ou=People,dc=example,dc=com"</mainIdentifier>
<defaultDelimiter>;</defaultDelimiter>
<defaultPolicy>FORCE</defaultPolicy>
<conditions>
<create>true</create>
<update>true</update>
<delete>true</delete>
<changeId>false</changeId>
</conditions>
<dataset>
<name>objectClass</name>
<policy>KEEP</policy>
<defaultValues></defaultValues>
<forceValues></forceValues>
<createValues>
<string>"inetorgperson"</string>
<string>"organizationalperson"</string>
<string>"person"</string>
<string>"top"</string>
</createValues>
<delimiter>,</delimiter>
</dataset>
<dataset>
<name>userPassword</name>
<policy>KEEP</policy>
<defaultValues>
<string>"change_this_2022"</string>
</defaultValues>
<forceValues></forceValues>
<createValues></createValues>
</dataset>
<dataset>
<name>displayname</name>
<policy>FORCE</policy>
<defaultValues>
</defaultValues>
<forceValues>
<string>js:srcBean.getDatasetFirstValueById("givenname") + " " + srcBean.getDatasetFirstValueById("sn").toUpperCase()
</string></forceValues>
<createValues></createValues>
</dataset>
</propertiesBasedSyncOptions>
</task>
Sur la destination on définit des opérations spécifiques sur certains attributs (ce sont les tags dataset) :
On voit dans les conditions que l'on gère les opérations de création, mise à jour et suppression.
bin/lsc -f /opt/lsc-2.1.6/etc/ -v
L'option -f- ou --config permet de définir le répertoire de configuration. Ceci peut être utilise si on veut gérer des versions bien distinctes des connecteurs et tâches.
LSC peut fonctionner en mode synchrone , asynchrone ou clean. Dans ce deuxième cas, LSC tourne en mode daemon et relance les tâches toutes les 5 secondes, avec un filtre de recherche sur l'attribut modifyTimestamp.
Note : il faudra penser dans ce cas à indexer l'attribut.
En mode clean, LSC va rechercher les objets qui ont disparu, et va les supprimer sur la destination.
Une fois que la configuration est faite, on peut lancer avec :
/path/to/lsc [-f /path/to/configuration] -s all # pour lancer toutes les tâches
/path/to/lsc -s task1,task3 # pour lancer les tâches nominativement, et dans l'ordre affiché
Par exemple, si on a modifié une entrée côté ODSEE :
bin/lsc -f /opt/lsc-2.1.6/etc -s odsee-people-2-rhds
.../...
Sep 19 14:16:05 - INFO - Logging configuration successfully loaded from /opt/lsc-2.1.6/etc/logback.xml
Sep 19 14:16:05 - INFO - LSC configuration successfully loaded from /opt/lsc-2.1.6/etc/
Sep 19 14:16:05 - INFO - Connecting to LDAP server ldap://rhds001:389/dc=example,dc=com as cn=Directory Manager
Sep 19 14:16:05 - INFO - Connecting to LDAP server ldap://odsee:1389/dc=example,dc=com as cn=Directory Manager
Sep 19 14:16:05 - INFO - Starting sync for odsee-people-2-rhds
Sep 19 14:16:12 - INFO - # Updating object uid=user.876,ou=People,dc=example,dc=com for odsee-people-2-rhds
# Mon Sep 19 14:16:12 UTC 2022
dn: uid=user.876,ou=People,dc=example,dc=com
changetype: modify
replace: l
l: Croix
-
replace: street
street: 876 Avenue de la république
-
replace: postalcode
postalcode: 59170
-
Sep 19 14:16:34 - INFO - All entries: 10154, to modify entries: 1, successfully modified entries: 1, errors: 0
La syntaxe est sensiblement la même :
/path/to/lsc -a all # pour lancer toutes les tâches
/path/to/lsc -a task1,task3 # pour lancer les tâches nominativement, et dans l'ordre affiché
Dans l'exemple ci-après, on voit que LSC démarre en mode asynchrone les 2 tâches :
/opt/lsc-2.1.6/bin/lsc -s odsee-people-2-rhds,rhds-people-2-odsee
.../...
Sep 22 15:14:58 - INFO - LSC configuration successfully loaded from /opt/lsc-2.1.6/bin/../etc/
Sep 22 15:14:58 - INFO - Connecting to LDAP server ldap://rhds001:389/dc=example,dc=com as cn=Directory Manager
Sep 22 15:14:58 - INFO - Connecting to LDAP server ldap://odsee:1389/dc=example,dc=com as cn=Directory Manager
Sep 22 15:14:58 - INFO - Starting async for odsee-people-2-rhds
Sep 22 15:14:58 - INFO - Starting async for rhds-people-2-odsee
Sep 22 15:15:16 - INFO - # Updating object uid=user.1298,ou=People,dc=example,dc=com for odsee-people-2-rhds
Sep 22 15:16:59 - INFO - # Updating object uid=user.1298,ou=People,dc=example,dc=com for rhds-people-2-odsee
# Thu Sep 22 15:16:59 UTC 2022
dn: uid=user.1298,ou=People,dc=example,dc=com
Comme les modifications se basent sur les valeurs d'attributs, il n'y a pas de risque de boucle infinie, sauf dans le cas où les attributs sont recalculés, avec des formules différentes.
Un script de démarrage (au format init.d) est disponible, afin de pouvoir automatiser le lancement en mode daemon.
Le lancement se fait avec l'option -c :
/path/to/lsc -c all # pour lancer toutes les tâches
/path/to/lsc -c task1,task3 # pour lancer les tâches nominativement, et dans l'ordre affiché
/opt/lsc-2.1.6/bin/lsc -c odsee-people-2-rhds
.../...
Sep 22 15:37:14 - INFO - Starting clean for odsee-people-2-rhds
Sep 22 15:37:15 - INFO - # Removing object uid=user.1298,ou=People,dc=example,dc=com for odsee-people-2-rhds
# Thu Sep 22 15:37:15 UTC 2022
dn: uid=user.1298,ou=People,dc=example,dc=com
changetype: delete
On peut ajouter l'option -c en mode synchrone. Par exemple :
/opt/lsc-2.1.6/bin/lsc -s odsee-people-2-rhds -c odsee-people-2-rhds
LSC project est une solution intéressante pour synchroniser des services hétérogènes. La configuration est assez explicite, la documentation est bien faite.
Si on veut avoir un fonctionnement en quasi temps réel, le mode asynchrone est à privilégier. Tout dépend donc du nombre de mises à jour réalisées sur les services.
Mais cette solution permet d'éviter de dépendre de produits éditeurs, ou d'installer un produit plus lourd type IDM.