Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ If Ansible does not support the `ldap_attrs` module you're using an old version
| dirsrv_replication_user_password | | Password for that account. | Yes | CB |
| dirsrv_begin_replication_immediately | true | Boolean, sets `nsds5ReplicaEnabled` to "on" or "off" in the replication agreement. This should be a safe: if you add a new server it won't start pushing its empty database to other servers in any case because they have a different generation ID and replication fails (see examples for more details), but if you want to be even safer or make some customizations to the replication agreement, set this to false | **No** | CB |
| dirsrv_consumer_referral_to_supplier | "ldap://supplier.example.com:389/" | Full LDAP URI including port. When a client tries to write to a consumer, which is read-only, it will redirect the client to this server (a supplier that can accept writes). | Yes | C |
| dirsrv_initial_sync_source | "" | FQDN of the server to initially sync from. If specified, this server will initiate a replica refresh to push data to the new server before enabling bidirectional replication. Leave empty to skip initial sync. | Yes | SB |
| dirsrv_wait_for_initial_sync | true | Whether to wait for the initial sync to complete before proceeding. Set to false for very large databases where you want to continue with the playbook and handle sync completion separately. | Yes | SB |
| dirsrv_initial_sync_timeout | 600 | Timeout in seconds to wait for initial sync to complete before giving up. | Yes | SB |
| dirsrv_enable_outbound_replication_after_sync | true | After initial sync completes, automatically enable outbound replication from this server back to the source. This ensures bidirectional replication only begins after the new server has received a complete copy of the data. | Yes | SB |

First of all choose if the server is a supplier, consumer or both and set
dirsv_role accordingly. Then set the variables related to that: look in the Role
Expand Down Expand Up @@ -167,6 +171,39 @@ to the consumer:
dirsrv_supplier_replica_id: 2
```

## Initial Synchronization in Multi-Master Setup

When adding a new server to an existing multi-master replication setup, it's important to ensure the new server receives a complete copy of the data before it starts replicating back to existing servers. This prevents the new server from potentially replicating an empty directory back to existing servers.

To safely add a new server:

1. Set `dirsrv_initial_sync_source` to the FQDN of an existing server containing the data
2. The role will:
- Configure the new server with replication agreements but initially disable outbound replication
- Trigger a replica refresh from the source server to the new server
- Wait for the initial sync to complete (controlled by `dirsrv_wait_for_initial_sync` and `dirsrv_initial_sync_timeout`)
- Enable outbound replication from the new server back to the source (if `dirsrv_enable_outbound_replication_after_sync` is true)

Example for adding a new server to an existing multi-master setup:

```yaml
- hosts: new_ldap_server
become: true
roles:
-
role: lvps.389ds_replication
dirsrv_replica_role: 'both'
dirsrv_suffix: "dc=example,dc=local"
dirsrv_server_uri: "ldap://localhost"
dirsrv_rootdn_password: secret
dirsrv_replication_user_password: "password1"
dirsrv_replication_user_password_remote: "password2"
dirsrv_consumer_uri: "ldap://existing_server.example.local:389/"
dirsrv_supplier_replica_id: 3
# Enable safe initial sync
dirsrv_initial_sync_source: "existing_server.example.local"
```

## Known bugs

If `dirsrv_replication_user_password` is changed, no change is reported: this
Expand Down
6 changes: 6 additions & 0 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ dirsrv_replication_user: Replication Manager
# dirsrv_replication_user_password: even-more-secret
dirsrv_begin_replication_immediately: true
# dirsrv_consumer_referral_to_supplier: "ldap://supplier.example.com:389/"

# For controlling initial synchronization
dirsrv_initial_sync_source: "" # FQDN of the server to initially sync from (if empty, no initial sync)
dirsrv_wait_for_initial_sync: true # Whether to wait for initial sync to complete
dirsrv_initial_sync_timeout: 600 # Timeout in seconds to wait for initial sync
dirsrv_enable_outbound_replication_after_sync: true # Enable outbound replication after sync completes
11 changes: 10 additions & 1 deletion tasks/configure_replication_agreement.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
# When setting up replication agreements, first set them up between all suppliers, then between the
# suppliers and the hubs, and last between the hub and the consumers.

- name: Determine if outbound replication should be enabled initially
ansible.builtin.set_fact:
dirsrv_outbound_replication_enabled: "{{
'on' if
(dirsrv_begin_replication_immediately and not dirsrv_initial_sync_source) or
(not dirsrv_initial_sync_source)
else 'off'
}}"

- name: Set agreement name
ansible.builtin.set_fact:
dirsrv_agreement_cn: "agreement_with_{{ dirsrv_consumer_uri | urlsplit('hostname') | replace('.', '_') }}"
Expand Down Expand Up @@ -34,7 +43,7 @@
description: "Agreement between this server (supplier) and {{ dirsrv_consumer_uri }} (consumer). Managed by Ansible."
nsds5replicatedattributelist: "{{ dirsrv_replica_attributes_list }}"
nsds5ReplicatedAttributeListTotal: "{{ dirsrv_replica_attributes_list_total }}"
nsds5ReplicaEnabled: "{{ 'on' if dirsrv_begin_replication_immediately else 'off' }}"
nsds5ReplicaEnabled: "{{ dirsrv_outbound_replication_enabled }}"
state: present

- name: Configure replication agreement on supplier
Expand Down
31 changes: 31 additions & 0 deletions tasks/enable_outbound_replication.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
- name: Create temporary LDIF file for enabling outbound replication
ansible.builtin.copy:
dest: "/tmp/dirsrv_enable_replication.ldif"
content: |
dn: cn=agreement_with_{{ dirsrv_consumer_uri | urlsplit('hostname') | replace('.', '_') }},cn=replica,cn="{{ dirsrv_suffix }}",cn=mapping tree,cn=config
changetype: modify
replace: nsds5ReplicaEnabled
nsds5ReplicaEnabled: on
mode: '0600'
when: dirsrv_perform_initial_sync and dirsrv_enable_outbound_replication_after_sync
Comment on lines +2 to +11
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a temporary file instead of community.general.ldap_attrs?


- name: Enable outbound replication after sync
ansible.builtin.command: >
ldapmodify
-x
{{ '-ZZ' if dirsrv_use_starttls else '' }}
-H {{ dirsrv_server_uri }}
-D "{{ dirsrv_rootdn }}"
-w "{{ dirsrv_rootdn_password }}"
-f /tmp/dirsrv_enable_replication.ldif
register: enable_result
changed_when: enable_result.rc == 0
failed_when: enable_result.rc != 0
when: dirsrv_perform_initial_sync and dirsrv_enable_outbound_replication_after_sync

- name: Remove temporary LDIF file for enabling replication
ansible.builtin.file:
path: "/tmp/dirsrv_enable_replication.ldif"
state: absent
when: dirsrv_perform_initial_sync and dirsrv_enable_outbound_replication_after_sync
14 changes: 14 additions & 0 deletions tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,17 @@
- name: Supplier or master/both
ansible.builtin.include_tasks: configure_replication_agreement.yml
when: dirsrv_instance_is_supplier

- name: Perform initial sync if source server is specified
ansible.builtin.include_tasks: perform_initial_sync.yml
when: dirsrv_instance_is_supplier and dirsrv_initial_sync_source | length > 0

- name: Enable outbound replication after initial sync
ansible.builtin.include_tasks: enable_outbound_replication.yml
when: dirsrv_instance_is_supplier and dirsrv_initial_sync_source | length > 0 and dirsrv_enable_outbound_replication_after_sync

- name: Restart service after replication agreement creation
ansible.builtin.include_tasks: restart_service.yml
when:
- dirsrv_instance_is_supplier or dirsrv_instance_is_consumer
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure, but isn't this going to restart the server even if the agreement is unchanged? If that's the case, is there any way to avoid it?

- dirsrv_initial_sync_source | length > 0 and dirsrv_enable_outbound_replication_after_sync
56 changes: 56 additions & 0 deletions tasks/perform_initial_sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
- name: Check if initial sync is required
ansible.builtin.set_fact:
dirsrv_perform_initial_sync: "{{ dirsrv_initial_sync_source | length > 0 }}"

- name: Create temporary LDIF file for initiating sync
ansible.builtin.copy:
dest: "/tmp/dirsrv_initial_sync.ldif"
content: |
dn: cn=agreement_with_{{ ansible_hostname + "_" + ansible_domain | replace('.', '_') }},cn=replica,cn="{{ dirsrv_suffix }}",cn=mapping tree,cn=config
changetype: modify
add: nsds5BeginReplicaRefresh
nsds5BeginReplicaRefresh: start
mode: '0600'
delegate_to: "{{ dirsrv_initial_sync_source }}"
when: dirsrv_perform_initial_sync
Comment on lines +6 to +16
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a temporary file instead of community.general.ldap_attrs?


- name: Trigger initial sync from source server
ansible.builtin.command: >
ldapmodify
-x
{{ '-ZZ' if dirsrv_use_starttls else '' }}
-H {{ dirsrv_server_uri | regex_replace(ansible_fqdn, dirsrv_initial_sync_source) }}
-D "{{ dirsrv_rootdn }}"
-w "{{ dirsrv_rootdn_password }}"
-f /tmp/dirsrv_initial_sync.ldif
delegate_to: "{{ dirsrv_initial_sync_source }}"
register: sync_result
changed_when: sync_result.rc == 0 and "Replica is being initialized" not in (sync_result.stderr | default(''))
failed_when: sync_result.rc != 0 and "Replica is being initialized" not in (sync_result.stderr | default(''))
when: dirsrv_perform_initial_sync

- name: Remove temporary LDIF file
ansible.builtin.file:
path: "/tmp/dirsrv_initial_sync.ldif"
state: absent
delegate_to: "{{ dirsrv_initial_sync_source }}"
when: dirsrv_perform_initial_sync

- name: Wait for initial sync to complete
ansible.builtin.command: >
ldapsearch
-x
{{ '-ZZ' if dirsrv_use_starttls else '' }}
-H {{ dirsrv_server_uri | regex_replace(ansible_fqdn, dirsrv_initial_sync_source) }}
-D "{{ dirsrv_rootdn }}"
-w "{{ dirsrv_rootdn_password }}"
-b "cn=agreement_with_{{ ansible_hostname + "_" + ansible_domain | replace('.', '_') }},cn=replica,cn={{ dirsrv_suffix | replace('=', '\=') | replace(',', '\,') }},cn=mapping tree,cn=config"
nsds5replicaUpdateInProgress
register: sync_status
changed_when: false
until: "'nsds5replicaUpdateInProgress: FALSE' in (sync_status.stdout | default(''))"
retries: "{{ (dirsrv_initial_sync_timeout / 10) | int }}"
delay: 10
delegate_to: "{{ dirsrv_initial_sync_source }}"
when: dirsrv_perform_initial_sync and dirsrv_wait_for_initial_sync
11 changes: 11 additions & 0 deletions tasks/restart_service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
- name: Restart local 389ds
ansible.builtin.service:
name: dirsrv@{{ dirsrv_serverid }}
state: restarted

- name: Restart remote consumer 389ds
ansible.builtin.service:
name: dirsrv@{{ dirsrv_serverid }}
state: restarted
delegate_to: "{{ dirsrv_consumer_uri | urlsplit('hostname') }}"
Loading