--- # ============================================================================= # NetBird v1.6 - User Provisioning # ============================================================================= # Creates users with embedded IdP and stores generated passwords. # Requires: service user PAT in vault.yml (see setup-bootstrap.yml) # # Run: # ansible-playbook -i inventory.yml setup-users.yml --ask-vault-pass # # Optional variables: # -e "dry_run=true" Preview changes without creating users # ============================================================================= - name: Provision NetBird Users hosts: netbird_servers become: false gather_facts: true vars_files: - group_vars/netbird_servers.yml - group_vars/vault.yml vars: # For SSL-IP mode, use server IP; for domain mode, use netbird_domain netbird_api_host: "{{ hostvars[inventory_hostname].ansible_host | default(netbird_domain) }}" netbird_api_url: "https://{{ netbird_api_host }}/api" dry_run: false pre_tasks: # ========================================================================= # Validate Prerequisites # ========================================================================= - name: Validate service PAT is provided ansible.builtin.assert: that: - vault_netbird_service_pat is defined - vault_netbird_service_pat | length > 0 fail_msg: | Service PAT not configured! Run setup-bootstrap.yml first, then add PAT to vault.yml - name: Verify API connectivity with PAT ansible.builtin.uri: url: "{{ netbird_api_url }}/users" method: GET headers: Authorization: "Token {{ vault_netbird_service_pat }}" Accept: "application/json" validate_certs: false status_code: [200] register: api_check delegate_to: localhost - name: Display connection status ansible.builtin.debug: msg: "API connection successful. Found {{ api_check.json | length }} existing users." tasks: # ========================================================================= # Fetch Existing State # ========================================================================= - name: Get existing users ansible.builtin.uri: url: "{{ netbird_api_url }}/users" method: GET headers: Authorization: "Token {{ vault_netbird_service_pat }}" Accept: "application/json" validate_certs: false status_code: [200] register: existing_users_response delegate_to: localhost - name: Extract existing user emails ansible.builtin.set_fact: existing_user_emails: "{{ existing_users_response.json | map(attribute='email') | list }}" - name: Get existing groups ansible.builtin.uri: url: "{{ netbird_api_url }}/groups" method: GET headers: Authorization: "Token {{ vault_netbird_service_pat }}" Accept: "application/json" validate_certs: false status_code: [200] register: existing_groups_response delegate_to: localhost - name: Build group ID mapping ansible.builtin.set_fact: group_id_map: "{{ group_id_map | default({}) | combine({item.name: item.id}) }}" loop: "{{ existing_groups_response.json }}" # ========================================================================= # Resolve Auto-Groups for Users # ========================================================================= - name: Resolve auto-groups for users ansible.builtin.set_fact: resolved_users: "{{ resolved_users | default([]) + [user_with_groups] }}" vars: battalion_group: >- {%- if item.battalion is defined and item.battalion -%} {%- if item.type | default('pilot') == 'pilot' -%} {{ item.battalion }}-pilots {%- else -%} {{ item.battalion }}-ground-stations {%- endif -%} {%- endif -%} final_auto_groups: >- {{ item.auto_groups | default([]) + ([battalion_group | trim] if battalion_group | trim else []) }} resolved_group_ids: >- {{ final_auto_groups | map('extract', group_id_map) | select('defined') | list }} user_with_groups: email: "{{ item.email }}" name: "{{ item.name }}" role: "{{ item.role | default('user') }}" auto_groups: "{{ resolved_group_ids }}" auto_group_names: "{{ final_auto_groups }}" battalion: "{{ item.battalion | default(none) }}" skip: "{{ item.email in existing_user_emails }}" loop: "{{ netbird_users | default([]) }}" # ========================================================================= # Display Plan # ========================================================================= - name: Count users to process ansible.builtin.set_fact: users_to_create: "{{ resolved_users | default([]) | rejectattr('skip') | list }}" users_to_skip: "{{ resolved_users | default([]) | selectattr('skip') | list }}" - name: Display provisioning plan ansible.builtin.debug: msg: | ============================================ User Provisioning Plan ============================================ Mode: {{ 'DRY RUN' if dry_run else 'EXECUTE' }} Users to CREATE ({{ users_to_create | length }}): {% for user in users_to_create %} - {{ user.email }} Name: {{ user.name }} Role: {{ user.role }} Groups: {{ user.auto_group_names | join(', ') or 'None' }} {% endfor %} {% if users_to_create | length == 0 %} (none) {% endif %} Users to SKIP - already exist ({{ users_to_skip | length }}): {% for user in users_to_skip %} - {{ user.email }} {% endfor %} {% if users_to_skip | length == 0 %} (none) {% endif %} ============================================ - name: End play in dry run mode ansible.builtin.meta: end_play when: dry_run | bool - name: End play if no users to create ansible.builtin.meta: end_play when: users_to_create | length == 0 # ========================================================================= # Create Users # ========================================================================= - name: Create credentials directory ansible.builtin.file: path: "{{ playbook_dir }}/files/credentials" state: directory mode: "0700" delegate_to: localhost - name: Create new users ansible.builtin.uri: url: "{{ netbird_api_url }}/users" method: POST headers: Authorization: "Token {{ vault_netbird_service_pat }}" Content-Type: "application/json" Accept: "application/json" body_format: json body: email: "{{ item.email }}" name: "{{ item.name }}" role: "{{ item.role }}" auto_groups: "{{ item.auto_groups }}" is_service_user: false validate_certs: false status_code: [200, 201] loop: "{{ users_to_create }}" register: created_users delegate_to: localhost # ========================================================================= # Store Credentials # ========================================================================= - name: Build credentials list ansible.builtin.set_fact: user_credentials: "{{ user_credentials | default([]) + [credential] }}" vars: matching_user: "{{ resolved_users | selectattr('email', 'equalto', item.json.email) | first }}" credential: email: "{{ item.json.email }}" name: "{{ item.json.name }}" password: "{{ item.json.password | default('N/A') }}" user_id: "{{ item.json.id }}" role: "{{ item.json.role }}" created_at: "{{ ansible_date_time.iso8601 }}" groups: "{{ matching_user.auto_group_names | default([]) }}" loop: "{{ created_users.results }}" when: item.json is defined - name: Save credentials to file ansible.builtin.copy: content: | --- # ============================================================================= # NetBird User Credentials # ============================================================================= # Generated: {{ ansible_date_time.iso8601 }} # Instance: {{ netbird_domain }} # # WARNING: Store securely! Passwords cannot be retrieved again. # ============================================================================= users: {% for user in user_credentials %} - email: "{{ user.email }}" name: "{{ user.name }}" password: "{{ user.password }}" user_id: "{{ user.user_id }}" role: "{{ user.role }}" groups: {% for group in user.groups %} - "{{ group }}" {% endfor %} {% if user.groups | length == 0 %} [] {% endif %} {% endfor %} dest: "{{ playbook_dir }}/files/credentials/users-{{ ansible_date_time.date }}.yml" mode: "0600" delegate_to: localhost when: - user_credentials is defined - user_credentials | length > 0 # ========================================================================= # Display Summary # ========================================================================= - name: Display provisioning summary ansible.builtin.debug: msg: | ============================================ User Provisioning Complete! ============================================ Created Users ({{ user_credentials | default([]) | length }}): {% for user in user_credentials | default([]) %} {{ user.email }} Password: {{ user.password }} Role: {{ user.role }} Groups: {{ user.groups | join(', ') or 'None' }} {% endfor %} Credentials saved to: {{ playbook_dir }}/files/credentials/users-{{ ansible_date_time.date }}.yml IMPORTANT: 1. Share passwords securely with users 2. Encrypt or delete credentials file after distribution: ansible-vault encrypt files/credentials/users-{{ ansible_date_time.date }}.yml 3. Users should change passwords on first login Login URL: https://{{ netbird_api_host }} ============================================ when: user_credentials is defined