Problem: Ansible to Manage UCS LDAP Containers by OU and Path

Ansible to Manage UCS LDAP Containers by OU and Path

Description:

When managing Univention Corporate Server (UCS) directory objects such as file shares or LDAP containers via Ansible, administrators may encounter situations where:

  • A share is unintentionally modified on a different server instead of being created in the targeted organizational unit (OU).
  • A container creation task fails with an error indicating that the object already exists, even though state: present is used.

This article explains the root cause of both behaviors and provides tested playbook adjustments to make Ansible tasks idempotent, predictable, and OU-aware.


Environment

  • Product: Univention Corporate Server (UCS) 5.0 / 5.2

  • Module: univention.ucs_modules.univention_directory_manager

  • Ansible Collection: univention.ucs_modules (tested with version 3.1.4)

  • Example hostnames:

    • Primary Directory Node: dc01.example.net
    • File Server: fs01.example.net
    • Organizational Units: ou=e176, ou=e110

Problem:

1. Unexpected Modification of a Share on a Different OU

When executing an Ansible task such as:

- name: "Create share Zweigleitung"
  delegate_to: "dc01.example.net"
  univention.ucs_modules.univention_directory_manager:
    module: "shares/share"
    state: "present"
    position: "cn=verwaltung,cn=shares,{{ OUDN }}"
    dn: "cn=Zweigleitung,cn=verwaltung,cn=shares,{{ OUDN }}"
    set_properties:
      - property: "name"
        value: "Zweigleitung"
      - property: "host"
        value: "{{ SERVERFQDN }}"
      - property: "path"
        value: "/home/{{ OU }}/v-shares/Zweigleitung"

the system log shows that the modification happened in a different OU (e.g., e110) instead of the intended OU e176:

directory_logger:
DN=cn=Zweigleitung,cn=verwaltung,cn=shares,ou=e110,dc=edu,dc=example,dc=net

2. Container Creation Fails with “Object already exists”

When repeatedly executing a task to create a container:

- name: "Create container for management shares"
  delegate_to: "dc01.example.net"
  univention.ucs_modules.univention_directory_manager:
    module: "container/cn"
    state: "present"
    position: "cn=shares,{{ OUDN }}"
    dn: "cn=verwaltung,cn=shares,{{ OUDN }}"
    set_properties:
      - property: "description"
        value: "Container for management shares"

the task fails on subsequent runs with:

Error creating 'container/cn' object: The object already exists:
cn=verwaltung,cn=shares,ou=e176,dc=edu,dc=example,dc=net

Despite state: present, the task is not idempotent.


Root Cause:

1. Share Modification Issue

The univention_directory_manager Ansible module internally performs a global existence check when using state: present.
For shares, the check is based on the property name (mapped to LDAP attribute cn), not on the full DN or the position you specify.

As a result:

  • If another share with the same name exists anywhere in LDAP (even under a different OU),
  • the module assumes it already exists and modifies that object instead of creating a new one.

This is why a share called “Zweigleitung” under ou=e110 was modified instead of creating a new one under ou=e176.


2. Container Creation Issue

For container/cn objects, the Ansible module checks existence differently:

  • When a dn is explicitly provided, it does not first check if the object exists.
  • It directly attempts to create it, leading to a “object exists” error on subsequent runs.
  • Without a proper filter or name property, the task is not idempotent.

Additionally, using a generic filter like (cn=verwaltung) can lead to collisions, because containers with the same CN may exist in different LDAP branches (e.g. cn=verwaltung,cn=groups,…).


Solutions and Playbook Adjustments:

A. Ensuring Correct Share Creation

Option 1: Use Globally Unique Names (Recommended for Creation)

To ensure the Ansible module creates a new share instead of modifying an existing one, define a globally unique name and dn:

- name: "Create share Zweigleitung in OU e176"
  delegate_to: "dc01.example.net"
  univention.ucs_modules.univention_directory_manager:
    module: "shares/share"
    state: "present"
    position: "cn=verwaltung,cn=shares,ou=e176,dc=edu,dc=example,dc=net"
    dn: "cn=Zweigleitung_e176,cn=verwaltung,cn=shares,ou=e176,dc=edu,dc=example,dc=net"
    set_properties:
      - property: "name"
        value: "Zweigleitung_e176"
      - property: "description"
        value: "Share for Zweigleitung (OU e176)"
      - property: "host"
        value: "{{ SERVERFQDN }}"
      - property: "path"
        value: "/home/e176/v-shares/Zweigleitung"
      - property: "group"
        value: "{{ GID_Zweigleitung.stdout }}"
      - property: "sambaValidUsers"
        value:
          - "{{ OU }}-Zweigleitung"
          - "{{ OU }}-Schulleitung"
      - property: "sambaVFSObjects"
        value: "recycle"

Option 2: Target Existing Shares Precisely with filter

If you want to modify an existing share rather than create a new one, use a filter that includes real LDAP attributes such as cn and univentionShareHost:

- name: "Modify existing share Zweigleitung on fs01"
  delegate_to: "dc01.example.net"
  univention.ucs_modules.univention_directory_manager:
    module: "shares/share"
    state: "present"
    position: "cn=verwaltung,cn=shares,ou=e110,dc=edu,dc=example,dc=net"
    filter: "(&(cn=Zweigleitung)(univentionShareHost=fs01.example.net))"
    set_properties:
      - property: "description"
        value: "Updated description for OU e110"

Note:
position defines the search base, and filter restricts the search within that base.
This combination ensures precise targeting without unwanted modifications in other OUs.


B. Creating Idempotent Containers

To avoid the “object exists” error, use state: present without dn, and let Ansible check for existence using a filter within the correct LDAP branch.

- name: "Ensure container 'verwaltung' for shares exists"
  delegate_to: "dc01.example.net"
  univention.ucs_modules.univention_directory_manager:
    module: "container/cn"
    state: "present"
    position: "cn=shares,{{ OUDN }}"
    filter: "(cn=verwaltung)"
    set_properties:
      - property: "name"
        value: "verwaltung"
      - property: "description"
        value: "Container for administrative shares"

Key Behavior Notes

  • The filter restricts the search to the position cn=shares,<OUDN>.
  • The module only creates the container if none exists there.
  • If another container named “verwaltung” exists elsewhere (e.g., under cn=groups), it will not be affected.

Alternative: Advanced Filter for OU-specific Containers

In complex UCS environments with multiple identical container names across different OUs, a more precise LDAP filter can be used.
This filter uses DN-based matching (:dn:) to ensure the container is only found or created under the correct path:

- name: "Ensure container 'verwaltung' exists only under shares and OU"
  delegate_to: "dc01.example.net"
  univention.ucs_modules.univention_directory_manager:
    module: "container/cn"
    state: "present"
    position: "cn=shares,{{ OUDN }}"
    filter: "(&(cn=verwaltung)(cn:dn:=shares)(ou:dn:={{ OU }}))"
    set_properties:
      - property: "name"
        value: "verwaltung"
      - property: "description"
        value: "Container for management shares under {{ OU }}"

Explanation

  • The :dn: syntax allows partial DN filtering (supported by UCS 5.0-5 errata613 and later).
  • It ensures that the match applies only within the current OU and cn=shares branch.
  • The task becomes idempotent — the container is created if missing, otherwise updated or skipped.

:warning: Note:
This advanced filter depends on correct UDM LDAP query handling.
For older UCS releases, fallback to the simpler filter (cn=verwaltung) may be required.


C. Verification and Validation

To ensure that the expected object was created or modified, add a verification step:

- name: "Verify share was created in correct OU"
  delegate_to: "dc01.example.net"
  univention.ucs_modules.univention_directory_manager:
    module: "shares/share"
    state: "info"
    filter: "(cn=Zweigleitung_e176)"
  register: share_info

- name: "Assert correct DN"
  ansible.builtin.assert:
    that:
      - "'cn=Zweigleitung_e176,cn=verwaltung,cn=shares,ou=e176,dc=edu,dc=example,dc=net' in share_info.result.dn"
    fail_msg: "Share was created or modified in the wrong OU!"

References


Conclusion

The observed behaviors are expected side effects of the Univention Ansible module’s object identification logic:

  • For shares, the name (cn) must be globally unique to ensure new creation.
  • For containers, idempotence requires using filter and position rather than dn.
  • For OU-specific precision, the extended filter with :dn: clauses provides a reliable and clean solution.

By applying these adjustments, administrators can ensure that:

  • Shares are created in the intended OU,
  • Containers are created only once,
  • and Ansible tasks remain safe, repeatable, and predictable.