restructure to roles

This commit is contained in:
Tomas Kloda
2026-03-27 07:30:45 +01:00
parent 1a215c7086
commit 1ec018ef40
19 changed files with 917 additions and 889 deletions
+18 -3
View File
@@ -587,9 +587,9 @@ The `examples/` directory contains complete playbook examples:
- `inventory_from_netbird.yml` - Export peers as Ansible inventory
- `peer_management.yml` - Manage and audit peers
## Config as Code (IaC Playbooks)
## Config as Code (IaC Roles)
This collection includes playbooks for managing your entire NetBird configuration as YAML files in Git — with preview/diff, strict mode, and automatic name-to-ID resolution.
This collection includes roles and playbooks for managing your entire NetBird configuration as YAML files in Git — with preview/diff, strict mode, and automatic name-to-ID resolution.
### Quick Start
@@ -613,6 +613,21 @@ ansible-playbook community.ansible_netbird.configure_netbird \
-e "commit=true"
```
### Using Roles Directly
For inventory-based workflows (e.g., AAP), use the roles directly in your own playbooks:
```yaml
- name: Configure NetBird
hosts: netbird_control_nodes
gather_facts: false
run_once: true
roles:
- role: community.ansible_netbird.configure
vars:
config_dir: "{{ playbook_dir }}/../netbird_config/{{ netbird_env }}"
```
### Features
- **Preview mode** (default) — shows a read-only diff of what would change before applying
@@ -620,7 +635,7 @@ ansible-playbook community.ansible_netbird.configure_netbird \
- **Name-based config** — use plain names ("developers") instead of API IDs; resolved automatically
- **Dependency ordering** — resources applied in correct order (settings → posture checks → groups → DNS → networks → policies)
- **Export utility** — captures current API state as clean, ready-to-use YAML config files
- **Inventory support** — set `target_hosts` to run against your inventory group instead of localhost
- **Roles** — use `community.ansible_netbird.configure` and `community.ansible_netbird.export` directly in your own playbooks for full control
### Config Directory Structure
+12 -6
View File
@@ -308,18 +308,24 @@ ansible-playbook community.ansible_netbird.configure_netbird \
-e "netbird_api_token=your-token"
```
Or create thin wrapper playbooks that set `target_hosts` for you:
Or create your own playbooks that use the roles directly — this is the recommended approach for inventory-based workflows:
```yaml
# configure_netbird.yml (wrapper)
- import_playbook: community.ansible_netbird.configure_netbird
vars:
target_hosts: netbird_control_nodes
config_dir: "{{ playbook_dir }}/../netbird_config/{{ netbird_env }}"
# configure_netbird.yml (using the role directly)
- name: Configure NetBird
hosts: netbird_control_nodes
gather_facts: false
run_once: true
roles:
- role: community.ansible_netbird.configure
vars:
config_dir: "{{ playbook_dir }}/../netbird_config/{{ netbird_env }}"
```
Then run with just a limit: `ansible-playbook configure_netbird.yml -i inventory -l preprod`
Using roles directly gives you full control over `hosts`, `gather_facts`, and variable resolution — and avoids `import_playbook` path resolution issues in AAP.
## Multi-Environment Setup
For managing multiple environments (e.g., production and staging), create separate config directories:
File diff suppressed because it is too large Load Diff
+7 -370
View File
@@ -1,386 +1,23 @@
---
# Playbook: Export Current NetBird Configuration
#
# Captures the current NetBird API state and produces clean, ready-to-use
# config files that can be used directly with the configure_netbird playbook.
# Also saves raw API data in a raw/ subdirectory for debugging.
# Thin wrapper around the community.ansible_netbird.export role.
# See the role documentation for full details.
#
# Usage:
# ansible-playbook community.ansible_netbird.export_netbird_config \
# -e "netbird_api_url=https://netbird.example.com" \
# -e "netbird_api_token=your-token"
#
# Output is written to /tmp/netbird_config_export/ by default.
# Override with: -e "export_dir=/custom/path"
#
# Required variables (via extra vars):
# - netbird_api_url: NetBird API URL (e.g., https://netbird.example.com)
# - netbird_api_token: NetBird API token
# Override output directory:
# ... -e "export_dir=/custom/path"
#
# Optional variables:
# - target_hosts: Inventory group or host to run on (default: localhost)
# - export_dir: Output directory (default: /tmp/netbird_config_export)
# - netbird_validate_certs: Validate API TLS certs (default: true)
# - target_hosts: Host or group to run on (default: localhost)
- name: Export Current NetBird Configuration
hosts: "{{ target_hosts | default('localhost') }}"
gather_facts: true
run_once: true
module_defaults:
group/community.ansible_netbird.netbird:
api_url: "{{ netbird_api_url }}"
api_token: "{{ netbird_api_token }}"
validate_certs: "{{ netbird_validate_certs }}"
vars:
# netbird_api_url: "" # Required - set via -e
# netbird_api_token: "" # Required - set via -e
netbird_validate_certs: true
export_dir: "/tmp/netbird_config_export"
pre_tasks:
- name: Validate API configuration
ansible.builtin.assert:
that:
- netbird_api_url is defined
- netbird_api_url | length > 0
- netbird_api_token is defined
- netbird_api_token | length > 0
fail_msg: |
NetBird API configuration is missing!
Required extra vars:
netbird_api_url: "https://netbird.example.com"
netbird_api_token: "your-api-token"
- name: Create export directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ export_dir }}"
- "{{ export_dir }}/access_control"
- "{{ export_dir }}/dns"
- "{{ export_dir }}/raw"
tasks:
# =========================================================================
# Fetch all resources from the NetBird API
# =========================================================================
- name: Fetch account settings
community.ansible_netbird.netbird_info:
resource: accounts
register: accounts_data
- name: Fetch groups
community.ansible_netbird.netbird_info:
resource: groups
register: groups_data
- name: Fetch posture checks
community.ansible_netbird.netbird_info:
resource: posture_checks
register: posture_checks_data
- name: Fetch setup keys
community.ansible_netbird.netbird_info:
resource: setup_keys
register: setup_keys_data
- name: Fetch DNS nameserver groups
community.ansible_netbird.netbird_info:
resource: dns_nameservers
register: dns_nameservers_data
- name: Fetch DNS settings
community.ansible_netbird.netbird_info:
resource: dns_settings
register: dns_settings_data
- name: Fetch DNS zones
community.ansible_netbird.netbird_info:
resource: dns_zones
register: dns_zones_data
- name: Fetch routes (deprecated API)
community.ansible_netbird.netbird_info:
resource: routes
register: routes_data
- name: Fetch networks
community.ansible_netbird.netbird_info:
resource: networks
register: networks_data
- name: Fetch routers for each network
ansible.builtin.uri:
url: "{{ netbird_api_url }}/api/networks/{{ item.id }}/routers"
method: GET
headers:
Authorization: "Token {{ netbird_api_token }}"
Accept: "application/json"
validate_certs: "{{ netbird_validate_certs }}"
return_content: true
loop: "{{ networks_data.data }}"
loop_control:
label: "{{ item.name }}"
register: networks_routers_raw
when: networks_data.data | length > 0
- name: Fetch resources for each network
ansible.builtin.uri:
url: "{{ netbird_api_url }}/api/networks/{{ item.id }}/resources"
method: GET
headers:
Authorization: "Token {{ netbird_api_token }}"
Accept: "application/json"
validate_certs: "{{ netbird_validate_certs }}"
return_content: true
loop: "{{ networks_data.data }}"
loop_control:
label: "{{ item.name }}"
register: networks_resources_raw
when: networks_data.data | length > 0
- name: Enrich networks with routers and resources
ansible.builtin.set_fact:
networks_enriched: >-
{% set result = [] -%}
{% for net in networks_data.data -%}
{% set routers = networks_routers_raw.results[loop.index0].get('json', []) -%}
{% set resources = networks_resources_raw.results[loop.index0].get('json', []) -%}
{% set _ = result.append(net | combine({'routers': routers, 'resources': resources})) -%}
{% endfor -%}
{{ result | to_json }}
when: networks_data.data | length > 0
- name: Set empty enriched networks when none exist
ansible.builtin.set_fact:
networks_enriched: []
when: networks_data.data | length == 0
- name: Fetch policies
community.ansible_netbird.netbird_info:
resource: policies
register: policies_data
- name: Fetch users
community.ansible_netbird.netbird_info:
resource: users
register: users_data
- name: Fetch peers
community.ansible_netbird.netbird_info:
resource: peers
register: peers_data
# =========================================================================
# Build ID-to-name lookup maps
# =========================================================================
- name: Initialize lookup maps
ansible.builtin.set_fact:
group_id_map: {}
posture_check_id_map: {}
peer_id_map: {}
- name: Build group ID-to-name map
ansible.builtin.set_fact:
group_id_map: "{{ group_id_map | combine({item.id: item.name}) }}"
loop: "{{ groups_data.data }}"
loop_control:
label: "{{ item.name }}"
when: groups_data.data | length > 0
- name: Build posture check ID-to-name map
ansible.builtin.set_fact:
posture_check_id_map: "{{ posture_check_id_map | combine({item.id: item.name}) }}"
loop: "{{ posture_checks_data.data }}"
loop_control:
label: "{{ item.name }}"
when: posture_checks_data.data | length > 0
- name: Build peer ID-to-name map
ansible.builtin.set_fact:
peer_id_map: "{{ peer_id_map | combine({item.id: item.name}) }}"
loop: "{{ peers_data.data }}"
loop_control:
label: "{{ item.name }}"
when: peers_data.data | length > 0
# =========================================================================
# Write clean config files (ready to use with configure_netbird playbook)
# =========================================================================
- name: Export clean settings
ansible.builtin.template:
src: templates/export/settings.yml.j2
dest: "{{ export_dir }}/settings.yml"
mode: "0644"
- name: Export clean groups
ansible.builtin.template:
src: templates/export/access_control/groups.yml.j2
dest: "{{ export_dir }}/access_control/groups.yml"
mode: "0644"
- name: Export clean posture checks
ansible.builtin.template:
src: templates/export/access_control/posture_checks.yml.j2
dest: "{{ export_dir }}/access_control/posture_checks.yml"
mode: "0644"
- name: Export clean policies
ansible.builtin.template:
src: templates/export/access_control/policies.yml.j2
dest: "{{ export_dir }}/access_control/policies.yml"
mode: "0644"
- name: Export clean setup keys
ansible.builtin.template:
src: templates/export/setup_keys.yml.j2
dest: "{{ export_dir }}/setup_keys.yml"
mode: "0644"
- name: Export clean DNS nameservers
ansible.builtin.template:
src: templates/export/dns/nameservers.yml.j2
dest: "{{ export_dir }}/dns/nameservers.yml"
mode: "0644"
- name: Export clean DNS zones
ansible.builtin.template:
src: templates/export/dns/zones.yml.j2
dest: "{{ export_dir }}/dns/zones.yml"
mode: "0644"
- name: Export clean DNS settings
ansible.builtin.template:
src: templates/export/dns/settings.yml.j2
dest: "{{ export_dir }}/dns/settings.yml"
mode: "0644"
- name: Export clean networks
ansible.builtin.template:
src: templates/export/networks.yml.j2
dest: "{{ export_dir }}/networks.yml"
mode: "0644"
- name: Export clean routes (deprecated API)
ansible.builtin.template:
src: templates/export/routes.yml.j2
dest: "{{ export_dir }}/routes.yml"
mode: "0644"
# =========================================================================
# Write raw API data (for debugging/reference)
# =========================================================================
- name: Export raw account settings
ansible.builtin.copy:
content: "{{ accounts_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/account_settings_raw.yml"
mode: "0644"
- name: Export raw groups
ansible.builtin.copy:
content: "{{ groups_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/groups_raw.yml"
mode: "0644"
- name: Export raw posture checks
ansible.builtin.copy:
content: "{{ posture_checks_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/posture_checks_raw.yml"
mode: "0644"
- name: Export raw setup keys
ansible.builtin.copy:
content: "{{ setup_keys_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/setup_keys_raw.yml"
mode: "0644"
- name: Export raw DNS
ansible.builtin.copy:
content: "{{ dns_nameservers_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/dns_nameservers_raw.yml"
mode: "0644"
- name: Export raw DNS zones
ansible.builtin.copy:
content: "{{ dns_zones_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/dns_zones_raw.yml"
mode: "0644"
- name: Export raw networks
ansible.builtin.copy:
content: "{{ networks_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/networks_raw.yml"
mode: "0644"
- name: Export raw routes
ansible.builtin.copy:
content: "{{ routes_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/routes_raw.yml"
mode: "0644"
- name: Export raw policies
ansible.builtin.copy:
content: "{{ policies_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/policies_raw.yml"
mode: "0644"
- name: Export users reference
ansible.builtin.copy:
content: |
---
# NetBird Users Reference ({{ users_data.data | length }} users)
# Exported on {{ ansible_date_time.date }}
# Users are managed via IdP sync, not via config-as-code.
{{ users_data.data | to_nice_yaml(indent=2) }}
dest: "{{ export_dir }}/raw/users_reference.yml"
mode: "0644"
- name: Export peers reference
ansible.builtin.copy:
content: |
---
# NetBird Peers Reference ({{ peers_data.data | length }} peers)
# Exported on {{ ansible_date_time.date }}
# Peers register via setup keys — not managed via config-as-code.
# Use peer IDs from this file for network router configuration.
{{ peers_data.data | to_nice_yaml(indent=2) }}
dest: "{{ export_dir }}/raw/peers_reference.yml"
mode: "0644"
# =========================================================================
# Summary
# =========================================================================
- name: Display export summary
ansible.builtin.debug:
msg:
- "Export complete! Files written to: {{ export_dir }}/"
- ""
- "Clean config files (ready to use with configure_netbird playbook):"
- " - settings.yml (account settings)"
- " - access_control/"
- " groups.yml ({{ groups_data.data | rejectattr('name', 'equalto', 'All') | rejectattr('issued', 'equalto', 'jwt') | list | length }} groups)"
- " posture_checks.yml ({{ posture_checks_data.data | length }} posture checks)"
- " policies.yml ({{ policies_data.data | length }} policies)"
- " - setup_keys.yml ({{ setup_keys_data.data | length }} setup keys — read-only reference)"
- " - dns/"
- " nameservers.yml ({{ dns_nameservers_data.data | length }} nameserver groups)"
- " zones.yml ({{ dns_zones_data.data | length }} zones)"
- " settings.yml (DNS settings)"
- " - networks.yml ({{ networks_data.data | length }} networks)"
- " - routes.yml ({{ routes_data.data | length }} routes — deprecated API)"
- ""
- "Raw API data (for debugging):"
- " raw/ (account, groups, posture_checks, setup_keys, dns, routes, networks, policies, users, peers)"
- ""
- "Next steps:"
- " 1. Review the clean config files"
- " 2. Copy them to your config directory (preserving directory structure)"
- " 3. Run the configure playbook to preview:"
- " ansible-playbook community.ansible_netbird.configure_netbird \\"
- " -e \"config_dir={{ export_dir }}\" \\"
- " -e \"netbird_api_url={{ netbird_api_url }}\" \\"
- " -e \"netbird_api_token=your-token\""
roles:
- community.ansible_netbird.export
+12
View File
@@ -0,0 +1,12 @@
---
# Required - path to your config directory (see config_skeleton/ for structure)
# config_dir: ""
# Apply changes (default: false — preview only)
commit: false
# Remove resources not in config (default: false)
strict: false
# Validate API TLS certificates
netbird_validate_certs: true
+476
View File
@@ -0,0 +1,476 @@
---
# Role: community.ansible_netbird.configure
#
# Applies NetBird logical configuration from YAML files via the NetBird REST API.
# Manages groups, policies, networks, DNS (nameservers + zones), posture checks,
# and account settings.
#
# Runs in preview mode by default — shows a read-only diff of what would change.
# To apply changes, set commit=true. Strict mode (strict=true) removes resources
# not defined in YAML.
#
# Execution order (respects dependencies):
# 1. Account settings (no dependencies)
# 2. Posture checks (no dependencies, needed by policies)
# 3. Groups (no dependencies, needed by everything else)
# 4. DNS (depend on groups)
# 5. Networks (depend on groups)
# 6. Policies (depend on groups + posture checks)
#
# Required variables:
# - config_dir: Path to your config directory
# - netbird_api_url: NetBird API URL
# - netbird_api_token: NetBird API token
#
# Optional variables:
# - commit: Apply changes (default: false)
# - strict: Remove resources not in config (default: false)
# - netbird_validate_certs: Validate API TLS certs (default: true)
# =========================================================================
# Validation (outside the module_defaults block so we get nice error messages
# if variables are undefined)
# =========================================================================
- name: Validate API configuration
ansible.builtin.assert:
that:
- netbird_api_url is defined
- netbird_api_url | length > 0
- netbird_api_token is defined
- netbird_api_token | length > 0
- config_dir is defined
- config_dir | length > 0
fail_msg: |
Configuration is missing!
Required variables:
config_dir: "/path/to/netbird_config"
netbird_api_url: "https://netbird.example.com"
netbird_api_token: "your-api-token"
- name: Display target environment
ansible.builtin.debug:
msg: >-
NetBird {{ config_dir | basename | upper }} —
{{ 'APPLY MODE' if commit | bool else 'PREVIEW MODE (read-only, no changes)' }}{{ ' + STRICT (unmanaged resources will be removed)' if strict | bool else '' }}
— config from {{ config_dir }}/
- name: Check config directory exists
ansible.builtin.stat:
path: "{{ config_dir }}"
register: config_dir_stat
delegate_to: localhost
- name: Fail if config directory doesn't exist
ansible.builtin.fail:
msg: "Config directory not found: {{ config_dir }}"
when: not config_dir_stat.stat.exists
# =========================================================================
# Load all configuration files
# =========================================================================
- name: Load settings
ansible.builtin.include_vars:
file: "{{ config_dir }}/settings.yml"
- name: Load access control (groups, posture checks, policies)
ansible.builtin.include_vars:
dir: "{{ config_dir }}/access_control"
extensions: ['yml', 'yaml']
- name: Load DNS configuration (nameservers, zones, settings)
ansible.builtin.include_vars:
dir: "{{ config_dir }}/dns"
extensions: ['yml', 'yaml']
- name: Load networks
ansible.builtin.include_vars:
file: "{{ config_dir }}/networks.yml"
- name: Display configuration summary
ansible.builtin.debug:
msg:
- "Configuration loaded from: {{ config_dir }}/"
- " Settings: {{ 'configured' if netbird_settings | default({}) | length > 0 else 'empty' }}"
- " Groups: {{ netbird_groups | default([]) | length }}"
- " Posture checks: {{ netbird_posture_checks | default([]) | length }}"
- " Policies: {{ netbird_policies | default([]) | length }}"
- " DNS nameservers: {{ netbird_dns_nameserver_groups | default([]) | length }}"
- " DNS zones: {{ netbird_dns_zones | default([]) | length }}"
- " Networks: {{ netbird_networks | default([]) | length }}"
# =========================================================================
# All API-calling tasks wrapped in a block with module_defaults
# =========================================================================
- name: Configure NetBird resources
block:
# =====================================================================
# PHASE 1: Apply foundational resources (commit mode only)
# =====================================================================
# --- 1. Settings (no dependencies) ---
- name: Apply settings
community.ansible_netbird.netbird_account:
peer_login_expiration_enabled: "{{ netbird_settings.peer_login_expiration_enabled | default(omit) }}"
peer_login_expiration: "{{ netbird_settings.peer_login_expiration | default(omit) }}"
peer_inactivity_expiration_enabled: "{{ netbird_settings.peer_inactivity_expiration_enabled | default(omit) }}"
peer_inactivity_expiration: "{{ netbird_settings.peer_inactivity_expiration | default(omit) }}"
regular_users_view_blocked: "{{ netbird_settings.regular_users_view_blocked | default(omit) }}"
groups_propagation_enabled: "{{ netbird_settings.groups_propagation_enabled | default(omit) }}"
jwt_groups_enabled: "{{ netbird_settings.jwt_groups_enabled | default(omit) }}"
jwt_groups_claim_name: "{{ netbird_settings.jwt_groups_claim_name | default(omit) }}"
jwt_allow_groups: "{{ netbird_settings.jwt_allow_groups | default([]) | map('extract', group_ids) | list if netbird_settings.jwt_allow_groups is defined else omit }}"
routing_peer_dns_resolution_enabled: "{{ netbird_settings.routing_peer_dns_resolution_enabled | default(omit) }}"
dns_domain: "{{ netbird_settings.dns_domain | default(omit) }}"
network_range: "{{ netbird_settings.network_range | default(omit) }}"
lazy_connection_enabled: "{{ netbird_settings.lazy_connection_enabled | default(omit) }}"
auto_update_always: "{{ netbird_settings.auto_update_always | default(omit) }}"
auto_update_version: "{{ netbird_settings.auto_update_version | default(omit) }}"
peer_expose_enabled: "{{ netbird_settings.peer_expose_enabled | default(omit) }}"
peer_expose_groups: "{{ netbird_settings.peer_expose_groups | default([]) | map('extract', group_ids) | list if netbird_settings.peer_expose_groups is defined else omit }}"
extra_peer_approval_enabled: "{{ netbird_settings.extra_peer_approval_enabled | default(omit) }}"
extra_user_approval_required: "{{ netbird_settings.extra_user_approval_required | default(omit) }}"
extra_network_traffic_logs_enabled: "{{ netbird_settings.extra_network_traffic_logs_enabled | default(omit) }}"
extra_network_traffic_logs_groups: "{{ netbird_settings.extra_network_traffic_logs_groups | default([]) | map('extract', group_ids) | list if netbird_settings.extra_network_traffic_logs_groups is defined else omit }}"
extra_network_traffic_packet_counter_enabled: "{{ netbird_settings.extra_network_traffic_packet_counter_enabled | default(omit) }}"
state: present
when:
- commit | bool
- netbird_settings | default({}) | length > 0
# --- 2. Posture Checks (no dependencies, needed by policies) ---
- name: Manage posture checks
community.ansible_netbird.netbird_posture_check:
name: "{{ item.name }}"
description: "{{ item.description | default(omit) }}"
checks: "{{ item.checks }}"
state: "{{ item.state | default('present') }}"
loop: "{{ netbird_posture_checks | default([]) }}"
loop_control:
label: "{{ item.name }}"
when:
- commit | bool
- netbird_posture_checks | default([]) | length > 0
# --- 3. Groups (no dependencies, needed by DNS/networks/policies) ---
- name: Manage groups
community.ansible_netbird.netbird_group:
name: "{{ item.name }}"
peers: "{{ item.peers | default(omit) }}"
state: "{{ item.state | default('present') }}"
loop: "{{ netbird_groups | default([]) }}"
loop_control:
label: "{{ item.name }}"
when:
- commit | bool
- netbird_groups | default([]) | length > 0
# =====================================================================
# PHASE 2: Fetch API state + build lookup maps (both modes)
# =====================================================================
- name: Initialize lookup maps
ansible.builtin.set_fact:
group_ids: {}
posture_check_ids: {}
- name: Fetch current groups for ID resolution
community.ansible_netbird.netbird_info:
resource: groups
register: api_groups
- name: Build group name→ID map
ansible.builtin.set_fact:
group_ids: "{{ group_ids | combine({item.name: item.id}) }}"
loop: "{{ api_groups.data }}"
loop_control:
label: "{{ item.name }}"
when: api_groups.data | length > 0
- name: Fetch current posture checks for ID resolution
community.ansible_netbird.netbird_info:
resource: posture_checks
register: api_posture_checks
- name: Build posture check name→ID map
ansible.builtin.set_fact:
posture_check_ids: "{{ posture_check_ids | combine({item.name: item.id}) }}"
loop: "{{ api_posture_checks.data }}"
loop_control:
label: "{{ item.name }}"
when: api_posture_checks.data | length > 0
- name: Display ID maps
ansible.builtin.debug:
msg:
- "Group IDs resolved: {{ group_ids | length }} groups"
- "Posture check IDs resolved: {{ posture_check_ids | length }} checks"
# =====================================================================
# Resolve group/posture-check names to IDs in config variables
# =====================================================================
- name: Resolve group names in policies and networks
ansible.builtin.set_fact:
_resolved_policies: >-
{% set result = [] -%}
{% for policy in netbird_policies | default([]) -%}
{% set resolved_rules = [] -%}
{% for rule in policy.rules | default([]) -%}
{% set _ = resolved_rules.append(rule | combine({
'sources': rule.sources | default([]) | map('extract', group_ids) | list,
'destinations': rule.destinations | default([]) | map('extract', group_ids) | list
})) -%}
{% endfor -%}
{% set _ = result.append(policy | combine({
'source_posture_checks': policy.source_posture_checks | default([]) | map('extract', posture_check_ids) | list,
'rules': resolved_rules
})) -%}
{% endfor -%}
{{ result | to_json }}
_resolved_networks: >-
{% set result = [] -%}
{% for network in netbird_networks | default([]) -%}
{% set resolved_resources = [] -%}
{% for resource in network.resources | default([]) -%}
{% set _ = resolved_resources.append(resource | combine({
'groups': resource.groups | default([]) | map('extract', group_ids) | list
})) -%}
{% endfor -%}
{% set resolved_routers = [] -%}
{% for router in network.routers | default([]) -%}
{% if router.peer_groups is defined -%}
{% set _ = resolved_routers.append(router | combine({
'peer_groups': router.peer_groups | map('extract', group_ids) | list
})) -%}
{% else -%}
{% set _ = resolved_routers.append(router) -%}
{% endif -%}
{% endfor -%}
{% set _ = result.append(network | combine({
'resources': resolved_resources,
'routers': resolved_routers
})) -%}
{% endfor -%}
{{ result | to_json }}
when: commit | bool
# =====================================================================
# PHASE 3: Fetch remaining API state (preview or strict mode)
# =====================================================================
- name: Fetch current DNS nameserver groups
community.ansible_netbird.netbird_info:
resource: dns_nameservers
register: api_dns_nameservers
when: not commit | bool or strict | bool
- name: Fetch current networks
community.ansible_netbird.netbird_info:
resource: networks
register: api_networks
when: not commit | bool or strict | bool
- name: Fetch current DNS zones
community.ansible_netbird.netbird_info:
resource: dns_zones
register: api_dns_zones
when: not commit | bool or strict | bool
- name: Fetch current policies
community.ansible_netbird.netbird_info:
resource: policies
register: api_policies
when: not commit | bool or strict | bool
- name: Fetch current account settings
community.ansible_netbird.netbird_info:
resource: accounts
register: api_accounts
when: not commit | bool or strict | bool
- name: Generate diff report
ansible.builtin.include_tasks: preview_diff_report.yml
when: not commit | bool
# =====================================================================
# PHASE 4: Apply dependent resources (commit mode only)
# =====================================================================
# --- 4. DNS (depend on groups) ---
- name: Manage DNS nameserver groups
community.ansible_netbird.netbird_dns:
resource_type: nameserver_group
name: "{{ item.name }}"
description: "{{ item.description | default(omit) }}"
nameservers: "{{ item.nameservers }}"
groups: "{{ item.groups | default([]) | map('extract', group_ids) | list }}"
domains: "{{ item.domains | default(omit) }}"
enabled: "{{ item.enabled | default(true) }}"
primary: "{{ item.primary | default(false) }}"
search_domains_enabled: "{{ item.search_domains_enabled | default(omit) }}"
state: "{{ item.state | default('present') }}"
loop: "{{ netbird_dns_nameserver_groups | default([]) }}"
loop_control:
label: "{{ item.name }}"
when:
- commit | bool
- netbird_dns_nameserver_groups | default([]) | length > 0
- name: Manage DNS settings
community.ansible_netbird.netbird_dns:
resource_type: settings
disabled_management_groups: "{{ netbird_dns_disabled_management_groups | default([]) | map('extract', group_ids) | list }}"
state: present
when:
- commit | bool
- netbird_dns_disabled_management_groups is defined
# --- 5. DNS Zones (depend on groups) ---
- name: Manage DNS zones
community.ansible_netbird.netbird_dns_zone:
name: "{{ item.name }}"
domain: "{{ item.domain | default(omit) }}"
enabled: "{{ item.enabled | default(true) }}"
enable_search_domain: "{{ item.enable_search_domain | default(false) }}"
distribution_groups: "{{ item.distribution_groups | default([]) | map('extract', group_ids) | list }}"
records: "{{ item.records | default(omit) }}"
state: "{{ item.state | default('present') }}"
loop: "{{ netbird_dns_zones | default([]) }}"
loop_control:
label: "{{ item.name }}"
when:
- commit | bool
- netbird_dns_zones | default([]) | length > 0
# --- 6. Networks (depend on groups) ---
- name: Manage networks
community.ansible_netbird.netbird_network:
name: "{{ item.name }}"
description: "{{ item.description | default(omit) }}"
routers: "{{ item.routers | default(omit) }}"
resources: "{{ item.resources | default(omit) }}"
state: "{{ item.state | default('present') }}"
loop: "{{ _resolved_networks | default(netbird_networks | default([])) }}"
loop_control:
label: "{{ item.name }}"
when:
- commit | bool
- netbird_networks | default([]) | length > 0
# --- 7. Policies (depend on groups + posture checks) ---
- name: Manage policies
community.ansible_netbird.netbird_policy:
name: "{{ item.name }}"
description: "{{ item.description | default('') }}"
enabled: "{{ item.enabled | default(true) }}"
source_posture_checks: "{{ item.source_posture_checks | default(omit) }}"
rules: "{{ item.rules | default(omit) }}"
state: "{{ item.state | default('present') }}"
loop: "{{ _resolved_policies | default(netbird_policies | default([])) }}"
loop_control:
label: "{{ item.name }}"
when:
- commit | bool
- netbird_policies | default([]) | length > 0
# =====================================================================
# PHASE 5: Strict mode — remove unmanaged resources
# =====================================================================
- name: "Strict: Build list of protected group names"
ansible.builtin.set_fact:
_protected_groups: "{{ api_groups.data | selectattr('issued', 'defined') | selectattr('issued', 'equalto', 'jwt') | map(attribute='name') | list + ['All'] }}"
_yaml_group_names: "{{ netbird_groups | default([]) | map(attribute='name') | list }}"
_yaml_pc_names: "{{ netbird_posture_checks | default([]) | map(attribute='name') | list }}"
_yaml_dns_names: "{{ netbird_dns_nameserver_groups | default([]) | map(attribute='name') | list }}"
_yaml_dns_zone_names: "{{ netbird_dns_zones | default([]) | map(attribute='name') | list }}"
_yaml_net_names: "{{ netbird_networks | default([]) | map(attribute='name') | list }}"
_yaml_pol_names: "{{ netbird_policies | default([]) | map(attribute='name') | list }}"
when: commit | bool and strict | bool
- name: "Strict: Identify unmanaged resources"
ansible.builtin.set_fact:
_orphaned_policies: "{{ api_policies.data | map(attribute='name') | list | difference(_yaml_pol_names) }}"
_orphaned_networks: "{{ api_networks.data | map(attribute='name') | list | difference(_yaml_net_names) }}"
_orphaned_dns: "{{ api_dns_nameservers.data | map(attribute='name') | list | difference(_yaml_dns_names) }}"
_orphaned_dns_zones: "{{ api_dns_zones.data | map(attribute='name') | list | difference(_yaml_dns_zone_names) }}"
_orphaned_posture_checks: "{{ api_posture_checks.data | map(attribute='name') | list | difference(_yaml_pc_names) }}"
_orphaned_groups: "{{ api_groups.data | map(attribute='name') | list | difference(_yaml_group_names) | difference(_protected_groups) }}"
when: commit | bool and strict | bool
- name: "Strict: Display resources to be removed"
ansible.builtin.debug:
msg:
- "Strict mode — removing unmanaged resources:"
- " Policies: {{ _orphaned_policies | length }} ({{ _orphaned_policies | join(', ') }})"
- " Networks: {{ _orphaned_networks | length }} ({{ _orphaned_networks | join(', ') }})"
- " DNS nameservers: {{ _orphaned_dns | length }} ({{ _orphaned_dns | join(', ') }})"
- " DNS zones: {{ _orphaned_dns_zones | length }} ({{ _orphaned_dns_zones | join(', ') }})"
- " Posture checks: {{ _orphaned_posture_checks | length }} ({{ _orphaned_posture_checks | join(', ') }})"
- " Groups: {{ _orphaned_groups | length }} ({{ _orphaned_groups | join(', ') }})"
when: commit | bool and strict | bool
- name: "Strict: Remove unmanaged policies"
community.ansible_netbird.netbird_policy:
name: "{{ item }}"
state: absent
loop: "{{ _orphaned_policies }}"
loop_control:
label: "{{ item }}"
when: commit | bool and strict | bool and _orphaned_policies | length > 0
- name: "Strict: Remove unmanaged networks"
community.ansible_netbird.netbird_network:
name: "{{ item }}"
state: absent
loop: "{{ _orphaned_networks }}"
loop_control:
label: "{{ item }}"
when: commit | bool and strict | bool and _orphaned_networks | length > 0
- name: "Strict: Remove unmanaged DNS nameserver groups"
community.ansible_netbird.netbird_dns:
resource_type: nameserver_group
name: "{{ item }}"
state: absent
loop: "{{ _orphaned_dns }}"
loop_control:
label: "{{ item }}"
when: commit | bool and strict | bool and _orphaned_dns | length > 0
- name: "Strict: Remove unmanaged DNS zones"
community.ansible_netbird.netbird_dns_zone:
name: "{{ item }}"
state: absent
loop: "{{ _orphaned_dns_zones }}"
loop_control:
label: "{{ item }}"
when: commit | bool and strict | bool and _orphaned_dns_zones | length > 0
- name: "Strict: Remove unmanaged posture checks"
community.ansible_netbird.netbird_posture_check:
name: "{{ item }}"
state: absent
loop: "{{ _orphaned_posture_checks }}"
loop_control:
label: "{{ item }}"
when: commit | bool and strict | bool and _orphaned_posture_checks | length > 0
- name: "Strict: Remove unmanaged groups"
community.ansible_netbird.netbird_group:
name: "{{ item }}"
state: absent
loop: "{{ _orphaned_groups }}"
loop_control:
label: "{{ item }}"
when: commit | bool and strict | bool and _orphaned_groups | length > 0
# =====================================================================
# Summary
# =====================================================================
- name: Configuration complete
ansible.builtin.debug:
msg: "NetBird configuration applied successfully from {{ config_dir }}/"
when: commit | bool
module_defaults:
group/community.ansible_netbird.netbird:
api_url: "{{ netbird_api_url }}"
api_token: "{{ netbird_api_token }}"
validate_certs: "{{ netbird_validate_certs }}"
@@ -1,10 +1,10 @@
---
# tasks/preview_diff_report.yml — Compute and display configuration diff report
#
# Included by configure_netbird.yml when commit=false (default).
# Included by the configure role when commit=false (default).
# Shows which resources will be added, removed, or re-applied.
#
# Expects these variables to be set by the parent playbook:
# Expects these variables to be set by the parent role:
# - api_groups, api_posture_checks, api_dns_nameservers, api_dns_zones,
# api_networks, api_policies, api_accounts (registered from netbird_info)
# - netbird_groups, netbird_posture_checks, netbird_dns_nameserver_groups,
@@ -38,10 +38,6 @@
# Classify resources: new / existing / remove
# =========================================================================
# Helper: extract present/absent names from a resource list
# "present" = no state defined OR state == present
# "absent" = state == absent
- name: Classify groups
ansible.builtin.set_fact:
groups_new: >-
+6
View File
@@ -0,0 +1,6 @@
---
# Output directory for exported config files
export_dir: "/tmp/netbird_config_export"
# Validate API TLS certificates
netbird_validate_certs: true
+375
View File
@@ -0,0 +1,375 @@
---
# Role: community.ansible_netbird.export
#
# Captures the current NetBird API state and produces clean, ready-to-use
# config files that can be used directly with the configure role/playbook.
# Also saves raw API data in a raw/ subdirectory for debugging.
#
# Required variables:
# - netbird_api_url: NetBird API URL (e.g., https://netbird.example.com)
# - netbird_api_token: NetBird API token
#
# Optional variables:
# - export_dir: Output directory (default: /tmp/netbird_config_export)
# - netbird_validate_certs: Validate API TLS certs (default: true)
#
# Note: This role uses ansible_date_time — ensure gather_facts is enabled
# in your playbook (gather_facts: true).
# =========================================================================
# Validation
# =========================================================================
- name: Validate API configuration
ansible.builtin.assert:
that:
- netbird_api_url is defined
- netbird_api_url | length > 0
- netbird_api_token is defined
- netbird_api_token | length > 0
fail_msg: |
NetBird API configuration is missing!
Required variables:
netbird_api_url: "https://netbird.example.com"
netbird_api_token: "your-api-token"
- name: Gather facts if needed
ansible.builtin.setup:
when: ansible_date_time is not defined
- name: Create export directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ export_dir }}"
- "{{ export_dir }}/access_control"
- "{{ export_dir }}/dns"
- "{{ export_dir }}/raw"
# =========================================================================
# All API-calling tasks wrapped in a block with module_defaults
# =========================================================================
- name: Export NetBird configuration
block:
# =====================================================================
# Fetch all resources from the NetBird API
# =====================================================================
- name: Fetch account settings
community.ansible_netbird.netbird_info:
resource: accounts
register: accounts_data
- name: Fetch groups
community.ansible_netbird.netbird_info:
resource: groups
register: groups_data
- name: Fetch posture checks
community.ansible_netbird.netbird_info:
resource: posture_checks
register: posture_checks_data
- name: Fetch setup keys
community.ansible_netbird.netbird_info:
resource: setup_keys
register: setup_keys_data
- name: Fetch DNS nameserver groups
community.ansible_netbird.netbird_info:
resource: dns_nameservers
register: dns_nameservers_data
- name: Fetch DNS settings
community.ansible_netbird.netbird_info:
resource: dns_settings
register: dns_settings_data
- name: Fetch DNS zones
community.ansible_netbird.netbird_info:
resource: dns_zones
register: dns_zones_data
- name: Fetch routes (deprecated API)
community.ansible_netbird.netbird_info:
resource: routes
register: routes_data
- name: Fetch networks
community.ansible_netbird.netbird_info:
resource: networks
register: networks_data
- name: Fetch routers for each network
ansible.builtin.uri:
url: "{{ netbird_api_url }}/api/networks/{{ item.id }}/routers"
method: GET
headers:
Authorization: "Token {{ netbird_api_token }}"
Accept: "application/json"
validate_certs: "{{ netbird_validate_certs }}"
return_content: true
loop: "{{ networks_data.data }}"
loop_control:
label: "{{ item.name }}"
register: networks_routers_raw
when: networks_data.data | length > 0
- name: Fetch resources for each network
ansible.builtin.uri:
url: "{{ netbird_api_url }}/api/networks/{{ item.id }}/resources"
method: GET
headers:
Authorization: "Token {{ netbird_api_token }}"
Accept: "application/json"
validate_certs: "{{ netbird_validate_certs }}"
return_content: true
loop: "{{ networks_data.data }}"
loop_control:
label: "{{ item.name }}"
register: networks_resources_raw
when: networks_data.data | length > 0
- name: Enrich networks with routers and resources
ansible.builtin.set_fact:
networks_enriched: >-
{% set result = [] -%}
{% for net in networks_data.data -%}
{% set routers = networks_routers_raw.results[loop.index0].get('json', []) -%}
{% set resources = networks_resources_raw.results[loop.index0].get('json', []) -%}
{% set _ = result.append(net | combine({'routers': routers, 'resources': resources})) -%}
{% endfor -%}
{{ result | to_json }}
when: networks_data.data | length > 0
- name: Set empty enriched networks when none exist
ansible.builtin.set_fact:
networks_enriched: []
when: networks_data.data | length == 0
- name: Fetch policies
community.ansible_netbird.netbird_info:
resource: policies
register: policies_data
- name: Fetch users
community.ansible_netbird.netbird_info:
resource: users
register: users_data
- name: Fetch peers
community.ansible_netbird.netbird_info:
resource: peers
register: peers_data
# =====================================================================
# Build ID-to-name lookup maps
# =====================================================================
- name: Initialize lookup maps
ansible.builtin.set_fact:
group_id_map: {}
posture_check_id_map: {}
peer_id_map: {}
- name: Build group ID-to-name map
ansible.builtin.set_fact:
group_id_map: "{{ group_id_map | combine({item.id: item.name}) }}"
loop: "{{ groups_data.data }}"
loop_control:
label: "{{ item.name }}"
when: groups_data.data | length > 0
- name: Build posture check ID-to-name map
ansible.builtin.set_fact:
posture_check_id_map: "{{ posture_check_id_map | combine({item.id: item.name}) }}"
loop: "{{ posture_checks_data.data }}"
loop_control:
label: "{{ item.name }}"
when: posture_checks_data.data | length > 0
- name: Build peer ID-to-name map
ansible.builtin.set_fact:
peer_id_map: "{{ peer_id_map | combine({item.id: item.name}) }}"
loop: "{{ peers_data.data }}"
loop_control:
label: "{{ item.name }}"
when: peers_data.data | length > 0
# =====================================================================
# Write clean config files (ready to use with configure role)
# =====================================================================
- name: Export clean settings
ansible.builtin.template:
src: export/settings.yml.j2
dest: "{{ export_dir }}/settings.yml"
mode: "0644"
- name: Export clean groups
ansible.builtin.template:
src: export/access_control/groups.yml.j2
dest: "{{ export_dir }}/access_control/groups.yml"
mode: "0644"
- name: Export clean posture checks
ansible.builtin.template:
src: export/access_control/posture_checks.yml.j2
dest: "{{ export_dir }}/access_control/posture_checks.yml"
mode: "0644"
- name: Export clean policies
ansible.builtin.template:
src: export/access_control/policies.yml.j2
dest: "{{ export_dir }}/access_control/policies.yml"
mode: "0644"
- name: Export clean setup keys
ansible.builtin.template:
src: export/setup_keys.yml.j2
dest: "{{ export_dir }}/setup_keys.yml"
mode: "0644"
- name: Export clean DNS nameservers
ansible.builtin.template:
src: export/dns/nameservers.yml.j2
dest: "{{ export_dir }}/dns/nameservers.yml"
mode: "0644"
- name: Export clean DNS zones
ansible.builtin.template:
src: export/dns/zones.yml.j2
dest: "{{ export_dir }}/dns/zones.yml"
mode: "0644"
- name: Export clean DNS settings
ansible.builtin.template:
src: export/dns/settings.yml.j2
dest: "{{ export_dir }}/dns/settings.yml"
mode: "0644"
- name: Export clean networks
ansible.builtin.template:
src: export/networks.yml.j2
dest: "{{ export_dir }}/networks.yml"
mode: "0644"
- name: Export clean routes (deprecated API)
ansible.builtin.template:
src: export/routes.yml.j2
dest: "{{ export_dir }}/routes.yml"
mode: "0644"
# =====================================================================
# Write raw API data (for debugging/reference)
# =====================================================================
- name: Export raw account settings
ansible.builtin.copy:
content: "{{ accounts_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/account_settings_raw.yml"
mode: "0644"
- name: Export raw groups
ansible.builtin.copy:
content: "{{ groups_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/groups_raw.yml"
mode: "0644"
- name: Export raw posture checks
ansible.builtin.copy:
content: "{{ posture_checks_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/posture_checks_raw.yml"
mode: "0644"
- name: Export raw setup keys
ansible.builtin.copy:
content: "{{ setup_keys_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/setup_keys_raw.yml"
mode: "0644"
- name: Export raw DNS
ansible.builtin.copy:
content: "{{ dns_nameservers_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/dns_nameservers_raw.yml"
mode: "0644"
- name: Export raw DNS zones
ansible.builtin.copy:
content: "{{ dns_zones_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/dns_zones_raw.yml"
mode: "0644"
- name: Export raw networks
ansible.builtin.copy:
content: "{{ networks_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/networks_raw.yml"
mode: "0644"
- name: Export raw routes
ansible.builtin.copy:
content: "{{ routes_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/routes_raw.yml"
mode: "0644"
- name: Export raw policies
ansible.builtin.copy:
content: "{{ policies_data.data | to_nice_yaml(indent=2) }}"
dest: "{{ export_dir }}/raw/policies_raw.yml"
mode: "0644"
- name: Export users reference
ansible.builtin.copy:
content: |
---
# NetBird Users Reference ({{ users_data.data | length }} users)
# Exported on {{ ansible_date_time.date }}
# Users are managed via IdP sync, not via config-as-code.
{{ users_data.data | to_nice_yaml(indent=2) }}
dest: "{{ export_dir }}/raw/users_reference.yml"
mode: "0644"
- name: Export peers reference
ansible.builtin.copy:
content: |
---
# NetBird Peers Reference ({{ peers_data.data | length }} peers)
# Exported on {{ ansible_date_time.date }}
# Peers register via setup keys — not managed via config-as-code.
# Use peer IDs from this file for network router configuration.
{{ peers_data.data | to_nice_yaml(indent=2) }}
dest: "{{ export_dir }}/raw/peers_reference.yml"
mode: "0644"
# =====================================================================
# Summary
# =====================================================================
- name: Display export summary
ansible.builtin.debug:
msg:
- "Export complete! Files written to: {{ export_dir }}/"
- ""
- "Clean config files (ready to use with configure role):"
- " - settings.yml (account settings)"
- " - access_control/"
- " groups.yml ({{ groups_data.data | rejectattr('name', 'equalto', 'All') | rejectattr('issued', 'equalto', 'jwt') | list | length }} groups)"
- " posture_checks.yml ({{ posture_checks_data.data | length }} posture checks)"
- " policies.yml ({{ policies_data.data | length }} policies)"
- " - setup_keys.yml ({{ setup_keys_data.data | length }} setup keys — read-only reference)"
- " - dns/"
- " nameservers.yml ({{ dns_nameservers_data.data | length }} nameserver groups)"
- " zones.yml ({{ dns_zones_data.data | length }} zones)"
- " settings.yml (DNS settings)"
- " - networks.yml ({{ networks_data.data | length }} networks)"
- " - routes.yml ({{ routes_data.data | length }} routes — deprecated API)"
- ""
- "Raw API data (for debugging):"
- " raw/ (account, groups, posture_checks, setup_keys, dns, routes, networks, policies, users, peers)"
- ""
- "Next steps:"
- " 1. Review the clean config files"
- " 2. Copy them to your config directory (preserving directory structure)"
- " 3. Run the configure role/playbook to preview"
module_defaults:
group/community.ansible_netbird.netbird:
api_url: "{{ netbird_api_url }}"
api_token: "{{ netbird_api_token }}"
validate_certs: "{{ netbird_validate_certs }}"