LDAP Username Management Plugin for Waldur Site Agent
Provisions POSIX users and groups in an LDAP directory when Waldur offering members need local accounts on an HPC site. Handles the full user lifecycle: account creation, access group membership, optional VPN password generation, and welcome email delivery.
At a glance
| Entry point | Group | Role |
|---|---|---|
ldap |
waldur_site_agent.username_management_backends |
username management |
ldap |
waldur_site_agent.backend_settings_schemas |
schema of the ldap: block (see below) |
Select it with username_management_backend: "ldap" on an offering whose processing
backend (usually slurm) needs POSIX accounts. It generates and looks up usernames,
creates and updates LDAP accounts and groups, syncs profiles, and releases accounts on
departure; it does not process orders or report usage itself.
Overview
graph LR
subgraph "Waldur Mastermind"
OU[Offering Users]
end
subgraph "Site Agent"
PROC[OfferingMembershipProcessor]
BACK[LdapUsernameBackend]
EMAIL[WelcomeEmailSender]
end
subgraph "LDAP Directory"
PEOPLE[ou=People]
GROUPS[ou=Groups]
end
subgraph "SMTP Gateway"
SMTP[Mail Server]
end
OU -->|"list offering users"| PROC
PROC -->|"get / create username"| BACK
BACK -->|"create user + group"| PEOPLE
BACK -->|"add to access groups"| GROUPS
BACK -->|"send welcome email"| EMAIL
EMAIL -->|"SMTP"| SMTP
classDef waldur fill:#e3f2fd
classDef agent fill:#f3e5f5
classDef ldap fill:#e8f5e9
classDef mail fill:#fff3e0
class OU waldur
class PROC,BACK,EMAIL agent
class PEOPLE,GROUPS ldap
class SMTP mail
Features
- POSIX User Provisioning: Creates
posixAccountentries with personal groups, auto-allocated UID/GID from configurable ranges - Username Generation: Multiple strategies —
first_initial_lastname(jsmith),first_letter_full_lastname(j.smith),firstname_dot_lastname(john.smith),firstname_lastname(johnsmith), or passthroughwaldur_username - Collision Resolution: Expands first-name prefix before falling back to numeric
suffixes (
j.smith→jo.smith→john.smith→j.smith2) - Access Groups: Automatically adds new users to configured LDAP groups
(e.g., VPN access, GPU access) with
memberUidormember(DN-based) attributes - VPN Password Generation: Optional cryptographically random password stored in
userPasswordattribute - Welcome Email: Templated email via SMTP with account credentials, delivered on user creation (opt-in)
- Profile Sync: Updates LDAP attributes (
givenName,sn,cn,mail) from Waldur user profiles - User Deactivation: Configurable removal or retention of LDAP entries when users leave the offering
Architecture
User Provisioning Flow
sequenceDiagram
participant W as Waldur API
participant P as MembershipProcessor
participant B as LdapUsernameBackend
participant L as LdapClient
participant S as SMTP Gateway
P->>W: List offering users
W-->>P: Offering users list
loop For each new user
P->>B: get_username(offering_user)
B->>L: search_user_by_email(email)
alt User found
L-->>B: Existing username
else Not found
B->>L: user_exists(waldur_username)
L-->>B: false
P->>B: generate_username(offering_user)
B->>B: Generate username string
B->>L: user_exists(candidate)
B->>B: Resolve collisions
B->>L: create_user(username, ...)
L->>L: get_next_uid / get_next_gid
L->>L: Create personal group
L->>L: Create posixAccount entry
L-->>B: uid_number
loop For each access group
B->>L: add_user_to_group(group, username)
end
opt Welcome email enabled
B->>S: Send templated email
end
end
end
Username Generation Strategy
graph TB
START[New offering user] --> FORMAT{username_format?}
FORMAT -->|first_initial_lastname| FI["jsmith"]
FORMAT -->|first_letter_full_lastname| FL["j.smith"]
FORMAT -->|firstname_dot_lastname| FD["john.smith"]
FORMAT -->|firstname_lastname| FN["johnsmith"]
FORMAT -->|waldur_username| WU["Waldur username as-is"]
FI --> UNIQUE
FL --> UNIQUE
FD --> UNIQUE
FN --> UNIQUE
WU --> UNIQUE
UNIQUE{Exists in LDAP?}
UNIQUE -->|No| DONE[Use username]
UNIQUE -->|"Yes (dot format)"| EXPAND["Expand prefix<br/>j.smith → jo.smith → john.smith"]
UNIQUE -->|"Yes (no dot / exhausted)"| SUFFIX["Numeric suffix<br/>jsmith2, jsmith3, ..."]
EXPAND --> DONE
SUFFIX --> DONE
classDef decision fill:#fff3e0
classDef result fill:#e8f5e9
class FORMAT,UNIQUE decision
class DONE result
Component Overview
graph TB
subgraph "LdapUsernameBackend"
GET[get_username<br/>Search by email, then Waldur username]
GEN[generate_username<br/>Create POSIX user + groups]
SYNC[sync_user_profiles<br/>Update LDAP attributes]
DEACT[deactivate_users<br/>Remove or retain users]
REL[release_users<br/>Release accounts on departure]
RECON[reconcile_offering<br/>Periodic offering-wide pass]
RPG[reconcile_project_groups<br/>Write Waldur's project groups]
end
subgraph "LdapClient"
SEARCH[Search Operations<br/>user / email / group lookups]
IDALLOC[ID Allocation<br/>next available UID / GID]
USEROP[User Operations<br/>create / delete / update]
GROUPOP[Group Operations<br/>create / delete / membership]
end
subgraph "WelcomeEmailSender"
RENDER[Jinja2 Template Rendering]
SEND[SMTP Delivery]
end
GET --> SEARCH
GEN --> IDALLOC
GEN --> USEROP
GEN --> GROUPOP
GEN --> RENDER
RENDER --> SEND
SYNC --> USEROP
DEACT --> USEROP
DEACT --> GROUPOP
REL --> USEROP
RECON --> USEROP
RECON --> GROUPOP
RPG --> GROUPOP
classDef backend fill:#e3f2fd
classDef client fill:#e8f5e9
classDef email fill:#fff3e0
class GET,GEN,SYNC,DEACT,REL,RECON,RPG backend
class SEARCH,IDALLOC,USEROP,GROUPOP client
class RENDER,SEND email
Configuration
Minimal Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Full Example (with welcome email and access groups)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
LDAP Settings Reference
| Setting | Required | Default | Description |
|---|---|---|---|
uri |
Yes | -- | LDAP server URI (e.g., ldap://ldap.example.com) |
bind_dn |
Yes | -- | DN to bind as (e.g., cn=admin,dc=example,dc=com) |
bind_password |
Yes | -- | Password for bind DN |
base_dn |
Yes | -- | Base DN for the directory |
use_starttls |
No | false |
Use STARTTLS for connection security |
people_ou |
No | ou=People |
OU for user entries |
groups_ou |
No | ou=Groups |
OU for personal and access groups; unused without either |
uid_range_start |
No | 10000 |
Start of UID allocation range |
uid_range_end |
No | 65000 |
End of UID allocation range |
gid_range_start |
No | 10000 |
Start of GID allocation range |
gid_range_end |
No | 65000 |
End of GID allocation range |
default_login_shell |
No | /bin/bash |
Default login shell for new users |
default_home_base |
No | /home |
Base path for home directories |
account_source |
No | ldap |
Who owns username/UID/GID: ldap (this agent) or waldur (see below) |
on_missing_posix_ids |
No | error |
error or skip when Waldur holds no ids (waldur mode) |
on_posix_mismatch |
No | report |
report, adopt or fail on an id disagreement (waldur mode) |
username_format |
No | first_initial_lastname |
Username strategy (see below); rejected in waldur mode |
waldur_username_attribute |
No | -- | Attribute for the Waldur username; the rename key (employeeNumber) |
remove_user_on_deactivate |
No | per account_source |
Release the entry once no live account remains; see below |
on_departure |
No | per account_source |
disable (park, keep ids) or delete; see below |
generate_vpn_password |
No | false |
Generate random VPN password on creation |
access_groups |
No | [] |
LDAP groups to add new users to |
personal_groups |
No | true |
Personal group per account; false needs waldur mode |
project_groups |
No | -- | Write Waldur's project groups (below) |
welcome_email |
No | -- | SMTP settings for welcome email (disabled when absent) |
Waldur-authoritative mode
By default this plugin owns the identity: it derives a username from the user's name and allocates a UID and GID by scanning the directory for free numbers. That breaks down as soon as one directory serves several offerings of the same service provider — each offering allocates independently, so the same person ends up with two UIDs behind one DN and one home directory, and two site agents fight over the entry.
Waldur already solves this on its side: a PosixIdPool attached to the service
provider gives each user one UID and one primary GID across every offering that
resolves to it. Setting account_source: waldur makes the agent write those
values rather than invent its own.
1 2 3 4 5 6 7 8 9 10 | |
In this mode the agent takes username, uidnumber, primarygroup,
home_directory and login_shell from the offering user and writes them into
the directory. username_format is rejected (Waldur names the accounts), and
uid_range_* is ignored for user accounts. The offering's
username_generation_policy must be anything other than service_provider
— under that policy Waldur expects the agent to assign names, which is the
opposite of what this mode does.
Provisioning runs as a reconcile loop on every membership cycle, over the full account list rather than only newly-requested ones, so an entry that was deleted or edited out of band is repaired on the next pass.
What happens in each case
| Directory state | Action |
|---|---|
| No entry, UID free | Create the entry, its personal group, and any access-group memberships |
| Entry matches Waldur | Nothing |
| Home directory, shell, name or mail differ | Rewritten to match Waldur |
uidNumber or gidNumber differ |
Governed by on_posix_mismatch |
| The entry carries another person's key (see below) | Error, the entry is left alone — always |
| The wanted UID is held by the same account under its old name | Renamed (see below) |
| The wanted UID is held by a different entry | Error, the account is left alone — always |
| Another entry shares the email address | Warning, and the account is created anyway |
The key. The agent ties an entry to its Waldur account by a stable key: the
person's Waldur username, written to waldur_username_attribute.
The attribute is reserved for the agent. It writes and rewrites it, so it must hold nothing else.
employeeNumber(aninetOrgPersonattribute, no schema change) is the usual choice; a site that keeps HR data inemployeeNumbermust pick another attribute.
The key comes from the offering user's user_username, which Waldur returns
only when the offering exposes usernames (its user attribute configuration; on
by default). If it does not, the agent writes no keys, recognises no renames,
and says so once per offering.
- Stamping. The key is written when the agent creates an entry, and onto an
existing entry only when its
uidanduidNumbermatch the account (so a directory adopted from before gets its keys on the first cycle). On that same id match a different value is rewritten -- the person's Waldur username changed -- unless the old value is another current account's Waldur username; each rewrite is logged with the old and new value. On every other path (drift, a held UID, a name match without theuidNumber, the rename lookup) a different value is a collision: logged, nothing written. The key is never written as a second copy of a key another entry carries. - Gating. An entry carrying a key that is not this account's is never updated, re-enabled, adopted or renumbered for it, whatever its ids say.
Renames. When Waldur's POSIX username for an account has no entry yet, the
agent looks for the entry carrying the account's key. If there is exactly one,
under another uid, with this account's uidNumber, it is moved in place
(modrdn uid=<old> to uid=<new>): uidNumber stays, homeDirectory and the
profile follow Waldur, the personal group is renamed with it, access-group
memberships move, and project groups follow from Waldur's member lists, all in
the same cycle. Renames run before the other accounts of the batch, and the old
entry is re-read just before the move.
The steps are ordered so that a failure at any point is finished by a later cycle and leaves nothing behind:
- the old name is recorded on the entry (a
descriptionvaluewaldur-site-agent:renamed-from=<old>), the new name is added to every group ingroups_outhat lists the old one, and the personal group is renamed -- nobody has lost access yet; modrdn uid=<old>touid=<new>;- the old name's memberships are removed, then the record.
A failure before step 2 is retried as a rename next cycle (the entry still carries the key under the old name); a failure in step 3 leaves the record, and the next cycle finishes the cleanup from it. If the old name has meanwhile been given to another entry, its memberships are left to that entry.
Refusals (reported as a collision with the reason, nothing written): two
entries carry the key; the key's entry has another uidNumber; the UID is held
by an entry with a missing or different key (another person, even one with the
same mail); the POSIX name and the Waldur username changed at once, so nothing
ties the old entry to the account.
Without waldur_username_attribute, renames are never followed: the agent says
so once, and a renamed account is reported as a collision until its entry is
renamed by hand. The decision is the same on the periodic pass and on a single
STOMP account event; when both race, the one that loses accepts the other's
rename only if the new entry carries the account's uidNumber and key.
The agent does not move files: a home directory path that embeds the old name needs moving by the site.
With stomp_enabled: true and no membership backend (an LDAP-only offering),
the agent subscribes to offering-user events for the LDAP backend, so account
changes reach the directory without waiting for the periodic reconcile.
on_posix_mismatch decides only the id-disagreement row:
report(default) logs a before/after diff and changes nothing. Renumbering a live account orphans every file it owns, so this is a human decision.adoptrewrites the entry and its personal group to Waldur's ids. Intended for a one-shot migration. Afterwards you mustchown -Rthe affected files from the old ids to the new ones — nothing else will.failraises instead of logging.
None of the three can move an account onto a UID another entry already holds.
LDAP does not enforce uidNumber uniqueness, so such a write would succeed and
leave two accounts owning the same files; that row is refused before policy is
consulted.
An account Waldur holds no ids for is never given a locally-invented one: that would reintroduce the double allocation this mode exists to remove. Attach a POSIX ID pool to the service provider, or turn POSIX accounts off for the offering.
When a user leaves
Upgrading: the departure sweep now runs every period, for every offering. In
event_processmode the periodic reconcile hands every offering user Waldur has put in a deletion state to the teardown on each cycle -- also on offerings with a membership backend (SLURM, say), where it previously ran only on cycles that also retried a stuck username. On the first cycle after the upgrade it therefore tears down, at once, every deletion Waldur queued up in the meantime; the log lineDeparture sweep for <offering>: N offering user(s) in a deletion statesays how many. Review the pending deletions in Waldur before upgrading if that matters.
Dropping the SLURM association is the resource backend's job. Releasing the
directory entry is this plugin's, and in this mode it is on by default:
remove_user_on_deactivate follows account_source, so under waldur an
unset value means release. A departed user who keeps a working POSIX login
on the cluster is exactly the gap this exists to close. Set
remove_user_on_deactivate: false explicitly to leave entries untouched (the
account is then logged as retained and Waldur is left waiting).
What release does is on_departure:
1 2 3 4 | |
disable(default underwaldur) parks the entry. The DN,uidNumber,gidNumberand personal group stay exactly as they are; the account is made unusable by settingloginShellto/usr/sbin/nologin, adding theshadowAccountclass withshadowExpire: 1(an expiry in the past), and dropping every group membership it still holds in access and project groups,memberUidand DN-stylememberalike. A membership that cannot be dropped fails the release instead: parking the entry while a group still lists it would report a teardown that left the access in place. A group that does not exist is not such a case — it grants nothing, and a typo'd or not-yet-created access group must not block a departure. A missing groups container is, though: a wronggroups_oumakes every lookup answer "no such object", so a sweep would come back empty and the release would report success having removed nothing. The agent checks the container before tolerating that answer. The agent recordsdescription: waldur-site-agent:disabledso a later reconcile can tell its own parked entries from ones an operator disabled by hand. This is the default because a uid must never be reused while files owned by it exist: keeping the entry keepsls -lhonest and keeps the pool's reservation and the directory in agreement.delete(default underldap, the historical meaning ofremove_user_on_deactivate) removes the entry, its personal group and its access-group memberships.
For disable to lock the account out, sssd on the nodes must honour the shadow
expiry — add to the [domain/...] section of sssd.conf:
1 | |
Coming back. Waldur re-mints the same username for a returning person (it
derives from the pool uid, which the provider-wide account keeps), so the
reconcile sees a live offering user whose entry exists but is parked. It
re-enables it in place — restores loginShell from Waldur, drops shadowExpire
and the marker, re-adds the configured access_groups — rather than failing on
UID_TAKEN or "entry exists". Project group membership comes back with the
SLURM association. An entry an operator disabled by hand (no marker) is treated
as an ordinary profile update, as before.
Because one directory serves every offering of the provider, "the user left
this offering" is not enough to act on. Before releasing, the plugin asks
Waldur for the person's other accounts on the same provider and keeps the
entry enabled if any account with the same username is still live — OK,
requested, creating or pending, restricted or not. A differently-named account
on a sibling offering is a separate entry and does not count. The check runs
against Waldur, never against the directory; a failed lookup keeps the entry.
The trigger is Waldur's own deletion request: the offering user must be in
Requested deletion, which Waldur sets when the person leaves their last
project on an offering that has offering_user_auto_deletion enabled. Without
that option the offering user stays OK and the entry is kept. The agent
notices the state on the next membership cycle (and immediately on the
offering-user update event under STOMP) and runs the deletion flow: SLURM
associations first, then this plugin's release, then core walks the offering
user through Deleting to Deleted so Waldur can release the provider-wide
identity behind it.
| Waldur says | Directory action |
|---|---|
| Same-named account still live on any offering of the provider | Kept, enabled |
| Only accounts in deletion states (or none) remain | Parked (disable) or removed (delete) |
| Entry already parked / absent (another offering's agent got there first) | Nothing to do |
Lookup fails, or the offering user carries no user_uuid |
Kept; raises, retried next cycle |
In the first three rows the offering user is then marked Deleted in Waldur; in
the last, core marks it Error deleting so the failure is visible until it goes.
Under account_source: ldap nothing changes unless remove_user_on_deactivate
is set to true, in which case the same provider-wide check applies and
on_departure defaults to delete.
Step-by-step setup
The Waldur side — creating the POSIX ID pool, choosing the username policy and prefix, enabling POSIX accounts and offering-user auto-deletion, and reading an offering user's uid and username back — is documented with screenshots in the Waldur user guide under Managing POSIX ID pools and Waldur-authoritative accounts in OpenLDAP. What follows is the agent and cluster side.
-
Prepare the directory. The agent creates user entries, personal groups and project groups; it does not create OUs or its own bind account. Create
ou=Peopleandou=Groupsunder the base DN and a bind DN with write access to both. Make sure thenisschema is loaded (posixAccount,posixGroup,shadowAccount). If access groups aregroupOfNames, also create the stand-in member (cn=nobody,<base_dn>by default,empty_group_member_dn). -
Pick a project-group GID range that does not overlap the pool's GID range (see below). The pool numbers people;
gid_range_*numbers project groups. -
Configure one
ldapblock and share it between every offering of the provider that uses the directory:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
.ldap: &ldap_settings uri: "ldaps://ldap.example.org" bind_dn: "cn=waldur-agent,dc=example,dc=org" bind_password: "<secret>" base_dn: "dc=example,dc=org" people_ou: "ou=People" groups_ou: "ou=Groups" account_source: "waldur" on_missing_posix_ids: "error" # error | skip on_posix_mismatch: "report" # report | adopt | fail on_departure: "disable" # disable (default here) | delete gid_range_start: 20000 # project groups only; keep clear of the pool gid_range_end: 29999 access_groups: - name: "cluster-users" offerings: - name: "Cluster A" waldur_api_url: "https://waldur.example.org/api/" waldur_api_token: "<token>" waldur_offering_uuid: "<offering A uuid>" backend_type: "slurm" username_management_backend: "ldap" order_processing_backend: "slurm" reporting_backend: "slurm" membership_sync_backend: "slurm" backend_settings: default_account: "root" customer_prefix: "c_" project_prefix: "p_" allocation_prefix: "a_" ldap: *ldap_settings - name: "Cluster B" # identical apart from the name and the offering uuid waldur_api_url: "https://waldur.example.org/api/" waldur_api_token: "<token>" waldur_offering_uuid: "<offering B uuid>" backend_type: "slurm" username_management_backend: "ldap" order_processing_backend: "slurm" reporting_backend: "slurm" membership_sync_backend: "slurm" backend_settings: default_account: "root" customer_prefix: "c_" project_prefix: "p_" allocation_prefix: "a_" ldap: *ldap_settingsLeave out
username_format(rejected in this mode) anduid_range_*(ignored for user accounts). -
Point SSSD on the nodes at the directory, and let it honour the shadow expiry the
disabledeparture mode relies on:1 2 3 4 5 6 7 8
[domain/ldap] id_provider = ldap ldap_uri = ldaps://ldap.example.org ldap_search_base = dc=example,dc=org ldap_user_search_base = ou=People,dc=example,dc=org ldap_group_search_base = ou=Groups,dc=example,dc=org ldap_schema = rfc2307 ldap_account_expire_policy = shadow -
Run a membership-sync cycle and verify each layer. The first cycle logs
LDAP reconcile: N created, ...; then:1 2 3 4 5 6 7
# Directory: one entry, ids as allocated in Waldur ldapsearch -x -H ldaps://ldap.example.org -b ou=People,dc=example,dc=org \ '(uid=hpc_100001)' uidNumber gidNumber homeDirectory loginShell # Node: SSSD resolves it getent passwd hpc_100001 && id hpc_100001 # Cluster: the association exists sacctmgr -P show association where user=hpc_100001 format=account,userRun the same checks after the second cluster's agent has had a cycle: the entry must be unchanged, with the same uid — that is what the shared pool buys.
-
Watch a departure and a return (see the section above for what happens). The log lines to expect are
Processing deletion of offering user ...,Disabled LDAP user ...(orDeleted LDAP user ...),Marked offering user ... DELETED in Waldur, and on returnRe-enabled LDAP user ... (uid N).
| Symptom | Fix |
|---|---|
Waldur returned no POSIX attributes ... |
Enable Manage POSIX/LDAP account; attach a pool to the provider |
Offering user X has no UID/primary GID in Waldur |
Predates the pool: re-save it, or on_missing_posix_ids: skip |
UID N is already held by LDAP user Y |
Pool overlaps existing entries: move the range, or adopt once |
| Departed user can still log in | SSSD needs ldap_account_expire_policy = shadow |
| Account stays Requested deletion | A teardown step keeps failing (see Teardown of offering user ... failed) |
Project group GIDs are still allocated locally
Only user accounts and their personal groups come from Waldur. Project and role
group GIDs written by the resource backend (the SLURM plugin's per-resource
groups) are still allocated from gid_range_start..gid_range_end. Project
groups kept per project, with GIDs Waldur allocates, are a separate feature:
see Project groups from Waldur.
These ranges must not overlap the offering's POSIX ID pool. LDAP does not enforce
gidNumberuniqueness, so an overlap silently produces two groups sharing a GID and files whose ownership is ambiguous. The agent cannot read the pool's bounds, so it warns at startup with the range it is configured with, but it cannot check this for you.
A note on settings validation
The agent core validates backend_settings against the schemas of the offering's
backend_type and its *_backend roles — not its username_management_backend.
The usual deployment pairs this plugin with slurm, whose schema allows unknown
keys, so this plugin's schema is never applied by core. The plugin therefore
validates its own ldap: block at construction, and a bad value fails the
backend rather than being silently ignored.
Username Formats
| Format | Example | Description |
|---|---|---|
first_initial_lastname |
jsmith |
First initial + full last name |
first_letter_full_lastname |
j.smith |
First initial + dot + full last name |
firstname_dot_lastname |
john.smith |
Full first name + dot + full last name |
firstname_lastname |
johnsmith |
Full first name + full last name |
waldur_username |
(as-is) | Use the Waldur username without transformation |
Names are normalized: diacritics removed (Müller → muller), non-alphanumeric
characters stripped. The waldur_username format bypasses normalization.
Welcome Email Settings
| Setting | Required | Default | Description |
|---|---|---|---|
smtp_host |
Yes | -- | SMTP server hostname |
smtp_port |
No | 587 |
SMTP server port |
smtp_username |
No | -- | SMTP auth username (omit for unauthenticated relay) |
smtp_password |
No | -- | SMTP auth password |
use_tls |
No | true |
Use STARTTLS (port 587) |
use_ssl |
No | false |
Use implicit SSL (port 465) |
timeout |
No | 30 |
SMTP connection timeout in seconds |
from_address |
Yes | -- | Sender email address |
from_name |
No | -- | Sender display name |
subject |
No | Your new account has been created |
Subject line (Jinja2 template) |
template_path |
Yes | -- | Path to Jinja2 email body template (absolute or relative to CWD) |
Welcome Email Template Variables
The following variables are available in the Jinja2 template:
| Variable | Description |
|---|---|
username |
The generated POSIX username |
vpn_password |
VPN password (empty string if generate_vpn_password is false) |
first_name |
User's first name from Waldur |
last_name |
User's last name from Waldur |
email |
User's email address |
home_directory |
Full home directory path (e.g., /home/jsmith) |
login_shell |
Configured login shell (e.g., /bin/bash) |
uid_number |
Allocated UID number |
Example templates are provided in examples/:
- welcome-email.txt.j2 — plain text
- welcome-email.html.j2 — HTML
Access Group Configuration
Each access group entry supports:
| Field | Required | Default | Description |
|---|---|---|---|
name |
Yes | -- | LDAP group name (e.g., vpnusrgroup) |
attribute |
No | memberUid |
Membership attribute: memberUid (UID) or member (DN) |
LDAP Object Classes
Default object classes can be overridden per deployment:
| Setting | Default | Description |
|---|---|---|
user_object_classes |
See below | Object classes for user entries |
user_group_object_classes |
See below | Object classes for personal user groups |
project_group_object_classes |
posixGroup, top |
Object classes for project groups |
Defaults:
- user_object_classes:
inetOrgPerson,organizationalPerson,person,posixAccount,top - user_group_object_classes:
groupOfNames,nsMemberOf,organizationalUnit,posixGroup,top
nsMemberOf is a 389 Directory Server class. On OpenLDAP, set
user_group_object_classes (for example ["top", "posixGroup"]), or turn
personal_groups off: with the default, every personal group, and so every new
account, is rejected.
Project groups from Waldur
Waldur keeps one group per project at each service provider: a name (the
project slug at creation, never renamed), a GID from the provider's POSIX ID
pools, and the usernames of the project's members who hold an account at the
provider. With project_groups.enabled the agent writes those groups into the
directory and lists them in the entries that grant cluster access, so a new
project's members can use a cluster without anyone editing the directory.
Example layout
A classic RFC 2307 (nis schema) directory, where posixGroup is structural and
allows memberUid only:
1 2 3 4 5 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
| Setting | Default | Description |
|---|---|---|
enabled |
false |
Write the project groups |
ou |
ou=projects |
OU, relative to base_dn, the groups are written under; it must exist |
object_classes |
["top", "posixGroup"] |
Classes of a new group; a memberless groupOfNames gets the stand-in |
member_attribute |
memberUid |
memberUid writes usernames; member writes uid=<name>,<people_ou>,<base_dn> |
membership |
sync |
sync adds and removes members to match Waldur; add_only never removes one |
on_gid_mismatch |
report |
Same-named entry with another GID: report keeps the GID, adopt renumbers |
managed_marker |
waldur-managed |
Extra description value on every group the agent creates or adopts |
parents |
[] |
Entries that list the DN of each group whose project has a resource on the offering |
parents[].dn |
-- | Full DN of the entry |
parents[].attribute |
member |
Attribute that holds the group DNs |
parents[].offering_uuids |
this offering | Offerings whose projects the entry lists (for a shared entry) |
What one cycle does
The pass runs on the agent's periodic cycle, after the account reconcile: the
periodic reconcile of event_process mode (every
WALDUR_SITE_AGENT_RECONCILIATION_PERIOD_MINUTES, also with STOMP off) and each
membership_sync pass. It runs even when the offering has no offering users
left, so parent entries are still cleaned up. Single-user events do not run it.
With STOMP on, the offering also subscribes to Waldur's
service_provider_project_group events: a group numbered, renumbered or
deleted, or project groups switched on or off for the provider. An event runs
the same pass a few seconds later, so a project's new group reaches the
directory without waiting for the periodic cycle. Events arriving close
together -- an import, a backfill numbering every group -- share one pass. The
pass always covers every group, never just the one an event names, since which
groups a parent lists is decided across all of them; and it never overlaps the
periodic pass on the same offering. Opting out with
stomp_membership_sync_enabled: false also drops this subscription.
It reads the provider's groups from Waldur
(GET /api/marketplace-service-provider-project-groups/?provider_offering_uuid=<offering>,
all pages, oldest first, following next-page links only to the configured
Waldur server). From the directory it reads, in one paged search each, the
project OU, every gidNumber under base_dn, every posixGroup under
base_dn, and the people OU (with account_source: waldur, or with
member_attribute: member). A search that ends in anything but success -- a
size, time or administrative limit -- aborts the pass rather than shrinking it.
Then, for each group:
| Directory state | Action |
|---|---|
| No entry, GID and name free | Create it with Waldur's name, GID, members and the marker |
| No entry, GID or name held (see below) | Report every cycle; nothing written, not added to parents |
| Entry with the same name and GID | Adopt it: add the marker, reconcile members and parents |
Same name, another GID, report |
Report every cycle; the GID stays, members and parents are reconciled |
Same name, another GID, adopt |
Rewrite gidNumber to Waldur's, unless it is held (then as report) |
| Waldur has no GID for the group | Skip it and log; its DN in a parent is left as it is |
A GID is held when any entry under base_dn carries it as gidNumber,
users' primary GIDs included. A name is held when a posixGroup of that
cn exists outside the project OU (a personal group, say), since NSS would
then see two groups of one name. The fix for either is in Waldur: set_gid to
a free value, or adopt the directory's GID -- adopting works only when the
group's name in Waldur equals the directory entry's cn, since that is what the
agent matches on.
adopt is for one moment only: after the files owned by the old GID have been
chgrp-ed to Waldur's. Renumbering a group any earlier orphans them, which is
why report is the default.
Every group the agent creates or adopts gets one extra description value,
managed_marker (waldur-managed by default). The operator's own description
values stay; description is multi-valued and allowed on posixGroup.
Each parent is made to list the DNs of the groups whose project has a resource on the parent's offerings. A DN is only ever removed when it lies under the project OU, and then:
- for a group Waldur lists, when its project has no resource on the parent's offerings, or the group could not be written because its GID or name is held (a write that fails for any other reason this cycle leaves the DN as it is);
- for a group Waldur no longer lists (an offering moved to another provider, say), only when its entry carries the marker.
An unmarked group under the project OU that Waldur does not list (a hand-made
benchmarking group, say) is never removed, nor is any DN from outside the
project OU. A groupOfNames that would lose its
last member gets empty_group_member_dn in the same modify.
Group entries are never deleted: their GIDs stay reserved in Waldur.
The directory is left untouched for the cycle when Waldur cannot be read (an HTTP error, an unreachable server, a failure on any page), when it lists no groups at all (the endpoint lists every group the provider ever had, so an empty answer is treated as broken), or when the listing changed while it was being paged through.
Members:
- With
account_source: waldur, a member is listed only once their account has an entry underpeople_outhat the account pass matched to it: withwaldur_username_attributeset, the entry must carry a key, and an account that pass could not reconcile cleanly (a UID or key collision, drift left underreport, a failed write) is not named in any group. Without the key attribute, only the last part applies. - With
membership: add_only, a user who is renamed in Waldur keeps the old username in the group alongside the new one; onlysyncswaps them. - With
member_attribute: member, the same holds in either mode: a DN must name an entry.
Two offerings of one provider on one directory see the same groups with the
same GIDs and members: the first agent creates a group, the second adopts it.
If they share a parent entry, list both offerings in parents[].offering_uuids
on both; otherwise each would take out the groups the other adds, and the
entry flaps every cycle. The agent warns once when offerings in its
configuration point at one parent DN with different effective sets (both
unset, or only one of them set).
The agent never allocates a project group GID; to pin or move one, use Waldur's
adopt and set_gid actions on the provider project group.
Personal groups
personal_groups: true (the default) keeps the historical layout: every account
gets cn=<username> in groups_ou with the account's primary GID. With false,
accounts are created and renumbered with the primary GID from Waldur and no
group entry, deletion leaves any group of the same name alone, and groups_ou
is only consulted for access_groups and the departure sweep, which skips it
when the OU does not exist. It requires account_source: waldur.
Plugin Structure
waldur_site_agent_ldap/ holds the username backend (backend.py), the welcome email
sender (email_sender.py), the Waldur-authoritative reconcile pass (reconcile.py),
project-group writing (project_groups.py) and the settings schema (schemas.py); the
LDAP connection itself comes from the shared ldap-client
package. Welcome email templates are in examples/, unit tests in tests/.
Entry Points
1 2 3 4 5 | |
Testing
1 2 3 4 5 6 7 8 | |
E2E Test Coverage
The LDAP E2E tests (plugins/slurm/tests/e2e/test_e2e_ldap.py) cover:
| Test Class | Focus |
|---|---|
TestLdapResourceLifecycle |
Create, update limits, terminate SLURM resource with LDAP integration |
TestLdapMembershipSync |
User provisioning, project groups, access groups, SLURM associations |
TestLdapUsageReporting |
Usage injection and verification with component mapper |
TestLdapBackwardCompat |
Passthrough vs conversion component mapping |
TestLdapWelcomeEmail |
Email sending, credential delivery, recipient validation |
TestLdapWaldurAuthoritative |
account_source: waldur — accounts and ids from Waldur, reconcile, departure |
TestLdapProviderProjectGroups |
Project groups written from Waldur's provider project groups |