--- # Tailscale Mesh Management # Validates mesh connectivity, manages keys, and monitors VPN performance # Run with: ansible-playbook -i hosts.ini playbooks/tailscale_mesh_management.yml - name: Tailscale Mesh Management hosts: all gather_facts: yes vars: tailscale_expected_nodes: - "homelab" - "atlantis" - "calypso" - "setillo" - "pi-5" - "pi-5-kevin" - "vish-concord-nuc" - "pve" - "truenas-scale" - "homeassistant" performance_test_targets: - "100.64.0.1" # Tailscale coordinator - "atlantis" - "calypso" tasks: - name: Check if Tailscale is installed command: which tailscale register: tailscale_installed failed_when: false changed_when: false - name: Get Tailscale status command: tailscale status --json register: tailscale_status_raw when: tailscale_installed.rc == 0 become: yes - name: Parse Tailscale status set_fact: tailscale_status: "{{ tailscale_status_raw.stdout | from_json }}" when: tailscale_installed.rc == 0 and tailscale_status_raw.stdout != "" - name: Get Tailscale IP command: tailscale ip -4 register: tailscale_ip when: tailscale_installed.rc == 0 become: yes - name: Display Tailscale node info debug: msg: | Tailscale Status for {{ inventory_hostname }}: - Installed: {{ 'Yes' if tailscale_installed.rc == 0 else 'No' }} {% if tailscale_installed.rc == 0 %} - IP Address: {{ tailscale_ip.stdout }} - Backend State: {{ tailscale_status.BackendState }} - Version: {{ tailscale_status.Version }} - Online: {{ tailscale_status.Self.Online }} - Exit Node: {{ tailscale_status.Self.ExitNode | default('None') }} {% endif %} - name: Get peer information set_fact: tailscale_peers: "{{ tailscale_status.Peer | dict2items | map(attribute='value') | list }}" when: tailscale_installed.rc == 0 and tailscale_status.Peer is defined - name: Analyze mesh connectivity set_fact: online_peers: "{{ tailscale_peers | selectattr('Online', 'equalto', true) | list }}" offline_peers: "{{ tailscale_peers | selectattr('Online', 'equalto', false) | list }}" expected_missing: "{{ tailscale_expected_nodes | difference(tailscale_peers | map(attribute='HostName') | list + [tailscale_status.Self.HostName]) }}" when: tailscale_installed.rc == 0 and tailscale_peers is defined - name: Display mesh analysis debug: msg: | Tailscale Mesh Analysis: - Total Peers: {{ tailscale_peers | length if tailscale_peers is defined else 0 }} - Online Peers: {{ online_peers | length if online_peers is defined else 0 }} - Offline Peers: {{ offline_peers | length if offline_peers is defined else 0 }} - Expected Nodes: {{ tailscale_expected_nodes | length }} - Missing Nodes: {{ expected_missing | length if expected_missing is defined else 0 }} {% if offline_peers is defined and offline_peers | length > 0 %} Offline Peers: {% for peer in offline_peers %} - {{ peer.HostName }} ({{ peer.TailscaleIPs[0] }}) {% endfor %} {% endif %} {% if expected_missing is defined and expected_missing | length > 0 %} Missing Expected Nodes: {% for node in expected_missing %} - {{ node }} {% endfor %} {% endif %} when: tailscale_installed.rc == 0 - name: Test connectivity to key nodes shell: | echo "=== Connectivity Tests ===" {% for target in performance_test_targets %} echo "Testing {{ target }}..." if ping -c 3 -W 2 {{ target }} >/dev/null 2>&1; then latency=$(ping -c 3 {{ target }} | tail -1 | awk -F '/' '{print $5}') echo "✓ {{ target }}: ${latency}ms avg" else echo "✗ {{ target }}: Unreachable" fi {% endfor %} register: connectivity_tests when: tailscale_installed.rc == 0 - name: Check Tailscale service status systemd: name: tailscaled register: tailscale_service when: tailscale_installed.rc == 0 become: yes - name: Get Tailscale logs shell: journalctl -u tailscaled --since "1 hour ago" --no-pager | tail -20 register: tailscale_logs when: tailscale_installed.rc == 0 become: yes - name: Check for Tailscale updates shell: | current_version=$(tailscale version | head -1 | awk '{print $1}') echo "Current version: $current_version" # Check if update is available (this is a simplified check) if command -v apt >/dev/null 2>&1; then apt list --upgradable 2>/dev/null | grep tailscale || echo "No updates available via apt" elif command -v yum >/dev/null 2>&1; then yum check-update tailscale 2>/dev/null || echo "No updates available via yum" else echo "Package manager not supported for update check" fi register: update_check when: tailscale_installed.rc == 0 become: yes - name: Generate network performance report shell: | echo "=== Network Performance Report ===" echo "Timestamp: $(date)" echo "Host: {{ inventory_hostname }}" echo "" {% if tailscale_installed.rc == 0 %} echo "=== Tailscale Interface ===" ip addr show tailscale0 2>/dev/null || echo "Tailscale interface not found" echo "" echo "=== Route Table ===" ip route | grep -E "(tailscale|100\.)" || echo "No Tailscale routes found" echo "" echo "=== DNS Configuration ===" tailscale status --peers=false --self=false 2>/dev/null | grep -E "(DNS|MagicDNS)" || echo "DNS info not available" {% else %} echo "Tailscale not installed on this host" {% endif %} register: performance_report when: tailscale_installed.rc == 0 - name: Check exit node configuration shell: tailscale status --json | jq -r '.ExitNodeStatus // "No exit node configured"' register: exit_node_status when: tailscale_installed.rc == 0 become: yes failed_when: false - name: Validate Tailscale ACLs (if admin) uri: url: "https://api.tailscale.com/api/v2/tailnet/{{ tailscale_tailnet | default('example.com') }}/acl" method: GET headers: Authorization: "Bearer {{ tailscale_api_key }}" register: acl_check when: - tailscale_api_key is defined - check_acls | default(false) | bool delegate_to: localhost run_once: true failed_when: false - name: Generate Tailscale mesh report copy: content: | # Tailscale Mesh Report - {{ inventory_hostname }} Generated: {{ ansible_date_time.iso8601 }} ## Node Status - Tailscale Installed: {{ 'Yes' if tailscale_installed.rc == 0 else 'No' }} {% if tailscale_installed.rc == 0 %} - IP Address: {{ tailscale_ip.stdout }} - Backend State: {{ tailscale_status.BackendState }} - Version: {{ tailscale_status.Version }} - Online: {{ tailscale_status.Self.Online }} - Service Status: {{ tailscale_service.status.ActiveState }} {% endif %} {% if tailscale_peers is defined %} ## Mesh Connectivity - Total Peers: {{ tailscale_peers | length }} - Online Peers: {{ online_peers | length }} - Offline Peers: {{ offline_peers | length }} ### Online Peers {% for peer in online_peers %} - {{ peer.HostName }} ({{ peer.TailscaleIPs[0] }}) - Last Seen: {{ peer.LastSeen }} {% endfor %} {% if offline_peers | length > 0 %} ### Offline Peers {% for peer in offline_peers %} - {{ peer.HostName }} ({{ peer.TailscaleIPs[0] }}) - Last Seen: {{ peer.LastSeen }} {% endfor %} {% endif %} {% endif %} ## Connectivity Tests ``` {{ connectivity_tests.stdout if connectivity_tests is defined else 'Not performed' }} ``` ## Performance Report ``` {{ performance_report.stdout if performance_report is defined else 'Not available' }} ``` ## Recent Logs ``` {{ tailscale_logs.stdout if tailscale_logs is defined else 'Not available' }} ``` ## Update Status ``` {{ update_check.stdout if update_check is defined else 'Not checked' }} ``` dest: "/tmp/tailscale_mesh_{{ inventory_hostname }}_{{ ansible_date_time.epoch }}.md" delegate_to: localhost - name: Display mesh summary debug: msg: | Tailscale Mesh Summary for {{ inventory_hostname }}: - Status: {{ 'Connected' if tailscale_installed.rc == 0 and tailscale_status.BackendState == 'Running' else 'Disconnected' }} - IP: {{ tailscale_ip.stdout if tailscale_installed.rc == 0 else 'N/A' }} - Peers: {{ tailscale_peers | length if tailscale_peers is defined else 0 }} - Report: /tmp/tailscale_mesh_{{ inventory_hostname }}_{{ ansible_date_time.epoch }}.md