256 lines
9.2 KiB
YAML
256 lines
9.2 KiB
YAML
---
|
|
# 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
|