--- - name: Tailscale Health Check (Homelab) hosts: active # or "all" if you want to check everything gather_facts: yes become: false vars: tailscale_bin: "/usr/bin/tailscale" tailscale_service: "tailscaled" tasks: - name: Verify Tailscale binary exists stat: path: "{{ tailscale_bin }}" register: ts_bin ignore_errors: true - name: Skip host if Tailscale not installed meta: end_host when: not ts_bin.stat.exists - name: Get Tailscale CLI version command: "{{ tailscale_bin }} version" register: ts_version changed_when: false failed_when: false - name: Get Tailscale status (JSON) command: "{{ tailscale_bin }} status --json" register: ts_status changed_when: false failed_when: false - name: Parse Tailscale JSON set_fact: ts_parsed: "{{ ts_status.stdout | from_json }}" when: ts_status.rc == 0 and (ts_status.stdout | length) > 0 and ts_status.stdout is search('{') - name: Extract important fields set_fact: ts_backend_state: "{{ ts_parsed.BackendState | default('unknown') }}" ts_ips: "{{ ts_parsed.Self.TailscaleIPs | default([]) }}" ts_hostname: "{{ ts_parsed.Self.HostName | default(inventory_hostname) }}" when: ts_parsed is defined - name: Report healthy nodes debug: msg: >- HEALTHY: {{ ts_hostname }} version={{ ts_version.stdout | default('n/a') }}, backend={{ ts_backend_state }}, ips={{ ts_ips }} when: - ts_parsed is defined - ts_backend_state == "Running" - ts_ips | length > 0 - name: Report unhealthy or unreachable nodes debug: msg: >- UNHEALTHY: {{ inventory_hostname }} rc={{ ts_status.rc }}, backend={{ ts_backend_state | default('n/a') }}, ips={{ ts_ips | default([]) }}, version={{ ts_version.stdout | default('n/a') }} when: ts_parsed is not defined or ts_backend_state != "Running" - name: Always print concise summary debug: msg: >- Host={{ inventory_hostname }}, Version={{ ts_version.stdout | default('n/a') }}, Backend={{ ts_backend_state | default('unknown') }}, IPs={{ ts_ips | default([]) }}