diff --git a/README.md b/README.md index ae8eb4a..00f2d08 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/guide_netbird_config_as_code.md b/docs/guide_netbird_config_as_code.md index 54e58f3..2624849 100644 --- a/docs/guide_netbird_config_as_code.md +++ b/docs/guide_netbird_config_as_code.md @@ -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: diff --git a/playbooks/configure_netbird.yml b/playbooks/configure_netbird.yml index 0563f8b..cebe2f3 100644 --- a/playbooks/configure_netbird.yml +++ b/playbooks/configure_netbird.yml @@ -1,526 +1,31 @@ --- # Playbook: Configure NetBird via API (Infrastructure as Code) # -# Applies NetBird logical configuration from YAML files. -# This playbook manages groups, policies, networks, DNS (nameservers + zones), -# posture checks, and account settings via the NetBird REST API. +# Thin wrapper around the community.ansible_netbird.configure role. +# See the role documentation for full details. # -# Runs in preview mode by default — shows a read-only diff of what would -# change. To apply changes, pass -e "commit=true". -# -# Strict mode (-e "strict=true") enforces full IaC: resources in the API -# that are NOT defined in YAML will be removed. Protected resources -# (All group, JWT groups, peers, users) are never deleted. -# -# 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) -# -# Usage (preview — default, read-only diff): +# Preview (default — read-only diff): # ansible-playbook community.ansible_netbird.configure_netbird \ # -e "config_dir=/path/to/netbird_config" \ # -e "netbird_api_url=https://netbird.example.com" \ # -e "netbird_api_token=your-token" # -# Usage (apply): +# Apply: # ansible-playbook community.ansible_netbird.configure_netbird \ # -e "config_dir=/path/to/netbird_config" \ # -e "netbird_api_url=https://netbird.example.com" \ # -e "netbird_api_token=your-token" \ # -e "commit=true" # -# Usage (apply + strict — full IaC enforcement): -# ansible-playbook community.ansible_netbird.configure_netbird \ -# -e "config_dir=/path/to/netbird_config" \ -# -e "netbird_api_url=https://netbird.example.com" \ -# -e "netbird_api_token=your-token" \ -# -e "commit=true" \ -# -e "strict=true" -# -# Required variables (via extra vars): -# - config_dir: Path to your config directory (see config_skeleton/ for structure) -# - netbird_api_url: NetBird API URL (e.g., https://netbird.example.com) -# - netbird_api_token: NetBird API token +# Apply + strict: +# ... -e "commit=true" -e "strict=true" # # Optional variables: -# - target_hosts: Inventory group or host to run on (default: localhost) -# Use this with inventory-based workflows, e.g.: -# -e "target_hosts=netbird_control_nodes" -l preprod -# - commit: Apply changes (default: false — preview only) -# - strict: Remove resources not in config (default: false) -# - netbird_validate_certs: Validate API TLS certs (default: true) -# -# Config directory structure: -# / -# ├── settings.yml → netbird_settings -# ├── networks.yml → netbird_networks -# ├── access_control/ -# │ ├── groups.yml → netbird_groups -# │ ├── posture_checks.yml → netbird_posture_checks -# │ └── policies.yml → netbird_policies -# └── dns/ -# ├── nameservers.yml → netbird_dns_nameserver_groups -# ├── zones.yml → netbird_dns_zones -# └── settings.yml → netbird_dns_disabled_management_groups +# - target_hosts: Host or group to run on (default: localhost) - name: Configure NetBird from IaC Definitions hosts: "{{ target_hosts | default('localhost') }}" gather_facts: false 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 - config_dir: "" # Required - path to your config directory - commit: false - strict: false - - 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 - - config_dir is defined - - config_dir | length > 0 - fail_msg: | - Configuration is missing! - Required extra vars: - 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 (aligned with NetBird UI sections) - # ========================================================================= - - 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 }}" - - tasks: - # ========================================================================= - # 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 - # Config files use plain names (e.g., "developers") — these are translated - # to API IDs here before being passed to modules. - # ========================================================================= - - 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: 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 (commit + strict only) - # Deletion order: reverse dependency (policies → networks → DNS → posture checks → groups) - # Protected: "All" group, JWT-issued groups, peers, users - # ========================================================================= - - 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 + roles: + - community.ansible_netbird.configure diff --git a/playbooks/export_netbird_config.yml b/playbooks/export_netbird_config.yml index cbd122d..3b94ea3 100644 --- a/playbooks/export_netbird_config.yml +++ b/playbooks/export_netbird_config.yml @@ -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 diff --git a/roles/configure/defaults/main.yml b/roles/configure/defaults/main.yml new file mode 100644 index 0000000..4e8ee18 --- /dev/null +++ b/roles/configure/defaults/main.yml @@ -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 diff --git a/roles/configure/tasks/main.yml b/roles/configure/tasks/main.yml new file mode 100644 index 0000000..ded4473 --- /dev/null +++ b/roles/configure/tasks/main.yml @@ -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 }}" diff --git a/playbooks/tasks/preview_diff_report.yml b/roles/configure/tasks/preview_diff_report.yml similarity index 98% rename from playbooks/tasks/preview_diff_report.yml rename to roles/configure/tasks/preview_diff_report.yml index 60e247c..468351e 100644 --- a/playbooks/tasks/preview_diff_report.yml +++ b/roles/configure/tasks/preview_diff_report.yml @@ -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: >- diff --git a/roles/export/defaults/main.yml b/roles/export/defaults/main.yml new file mode 100644 index 0000000..10ca18a --- /dev/null +++ b/roles/export/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# Output directory for exported config files +export_dir: "/tmp/netbird_config_export" + +# Validate API TLS certificates +netbird_validate_certs: true diff --git a/roles/export/tasks/main.yml b/roles/export/tasks/main.yml new file mode 100644 index 0000000..547e8d3 --- /dev/null +++ b/roles/export/tasks/main.yml @@ -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 }}" diff --git a/playbooks/templates/export/access_control/groups.yml.j2 b/roles/export/templates/export/access_control/groups.yml.j2 similarity index 100% rename from playbooks/templates/export/access_control/groups.yml.j2 rename to roles/export/templates/export/access_control/groups.yml.j2 diff --git a/playbooks/templates/export/access_control/policies.yml.j2 b/roles/export/templates/export/access_control/policies.yml.j2 similarity index 100% rename from playbooks/templates/export/access_control/policies.yml.j2 rename to roles/export/templates/export/access_control/policies.yml.j2 diff --git a/playbooks/templates/export/access_control/posture_checks.yml.j2 b/roles/export/templates/export/access_control/posture_checks.yml.j2 similarity index 100% rename from playbooks/templates/export/access_control/posture_checks.yml.j2 rename to roles/export/templates/export/access_control/posture_checks.yml.j2 diff --git a/playbooks/templates/export/dns/nameservers.yml.j2 b/roles/export/templates/export/dns/nameservers.yml.j2 similarity index 100% rename from playbooks/templates/export/dns/nameservers.yml.j2 rename to roles/export/templates/export/dns/nameservers.yml.j2 diff --git a/playbooks/templates/export/dns/settings.yml.j2 b/roles/export/templates/export/dns/settings.yml.j2 similarity index 100% rename from playbooks/templates/export/dns/settings.yml.j2 rename to roles/export/templates/export/dns/settings.yml.j2 diff --git a/playbooks/templates/export/dns/zones.yml.j2 b/roles/export/templates/export/dns/zones.yml.j2 similarity index 100% rename from playbooks/templates/export/dns/zones.yml.j2 rename to roles/export/templates/export/dns/zones.yml.j2 diff --git a/playbooks/templates/export/networks.yml.j2 b/roles/export/templates/export/networks.yml.j2 similarity index 100% rename from playbooks/templates/export/networks.yml.j2 rename to roles/export/templates/export/networks.yml.j2 diff --git a/playbooks/templates/export/routes.yml.j2 b/roles/export/templates/export/routes.yml.j2 similarity index 100% rename from playbooks/templates/export/routes.yml.j2 rename to roles/export/templates/export/routes.yml.j2 diff --git a/playbooks/templates/export/settings.yml.j2 b/roles/export/templates/export/settings.yml.j2 similarity index 100% rename from playbooks/templates/export/settings.yml.j2 rename to roles/export/templates/export/settings.yml.j2 diff --git a/playbooks/templates/export/setup_keys.yml.j2 b/roles/export/templates/export/setup_keys.yml.j2 similarity index 100% rename from playbooks/templates/export/setup_keys.yml.j2 rename to roles/export/templates/export/setup_keys.yml.j2