Files
homelab-optimized/ansible/automation/playbooks/network_connectivity.yml
Gitea Mirror Bot 5cbaedc119
Some checks failed
Documentation / Build Docusaurus (push) Failing after 17m43s
Documentation / Deploy to GitHub Pages (push) Has been skipped
Sanitized mirror from private repository - 2026-03-31 12:23:18 UTC
2026-03-31 12:23:18 +00:00

235 lines
8.4 KiB
YAML

---
# Network Connectivity Playbook
# Full mesh connectivity check: Tailscale status, ping matrix, SSH port reachability,
# HTTP endpoint checks, and per-host JSON reports.
# Usage: ansible-playbook playbooks/network_connectivity.yml
# Usage: ansible-playbook playbooks/network_connectivity.yml -e "host_target=synology"
- name: Network Connectivity Check
hosts: "{{ host_target | default('active') }}"
gather_facts: yes
ignore_unreachable: true
vars:
ntfy_url: "{{ ntfy_url | default('https://ntfy.sh/REDACTED_TOPIC') }}"
report_dir: "/tmp/connectivity_reports"
ts_candidates:
- /usr/bin/tailscale
- /var/packages/Tailscale/target/bin/tailscale
http_endpoints:
- name: Portainer
url: "http://100.67.40.126:9000"
- name: Gitea
url: "http://100.67.40.126:3000"
- name: Immich
url: "http://100.67.40.126:2283"
- name: Home Assistant
url: "http://100.112.186.90:8123"
tasks:
# ---------- Setup ----------
- name: Create connectivity report directory
ansible.builtin.file:
path: "{{ report_dir }}"
state: directory
mode: '0755'
delegate_to: localhost
run_once: true
# ---------- Tailscale detection ----------
- name: Detect Tailscale binary path (first candidate that exists)
ansible.builtin.shell: |
for p in {{ ts_candidates | join(' ') }}; do
[ -x "$p" ] && echo "$p" && exit 0
done
echo ""
register: ts_bin
changed_when: false
failed_when: false
- name: Get Tailscale status JSON (if binary found)
ansible.builtin.command: "{{ ts_bin.stdout }} status --json"
register: ts_status_raw
changed_when: false
failed_when: false
when: ts_bin.stdout | length > 0
- name: Parse Tailscale status JSON
ansible.builtin.set_fact:
ts_parsed: "{{ ts_status_raw.stdout | from_json }}"
when:
- ts_bin.stdout | length > 0
- ts_status_raw.rc is defined
- ts_status_raw.rc == 0
- ts_status_raw.stdout | length > 0
- ts_status_raw.stdout is search('{')
- name: Extract Tailscale BackendState and first IP
ansible.builtin.set_fact:
ts_backend_state: "{{ ts_parsed.BackendState | default('unknown') }}"
ts_first_ip: "{{ (ts_parsed.Self.TailscaleIPs | default([]))[0] | default('n/a') }}"
when: ts_parsed is defined
- name: Set Tailscale defaults when binary not found or parse failed
ansible.builtin.set_fact:
ts_backend_state: "{{ ts_backend_state | default('not_installed') }}"
ts_first_ip: "{{ ts_first_ip | default('n/a') }}"
# ---------- Ping matrix (all active hosts except self) ----------
- name: Ping all other active hosts (2 pings, 2s timeout)
ansible.builtin.command: >
ping -c 2 -W 2 {{ hostvars[item]['ansible_host'] }}
register: ping_results
loop: "{{ groups['active'] | difference([inventory_hostname]) }}"
loop_control:
label: "{{ item }} ({{ hostvars[item]['ansible_host'] }})"
changed_when: false
failed_when: false
- name: Build ping summary map
ansible.builtin.set_fact:
ping_map: >-
{{
ping_map | default({}) | combine({
item.item: {
'host': hostvars[item.item]['ansible_host'],
'rc': item.rc,
'status': 'OK' if item.rc == 0 else 'FAIL'
}
})
}}
loop: "{{ ping_results.results }}"
loop_control:
label: "{{ item.item }}"
- name: Identify failed ping targets
ansible.builtin.set_fact:
failed_ping_peers: >-
{{
ping_results.results
| selectattr('rc', 'ne', 0)
| map(attribute='item')
| list
}}
# ---------- SSH port reachability ----------
- name: Check SSH port reachability for all other active hosts
ansible.builtin.command: >
nc -z -w 3
{{ hostvars[item]['ansible_host'] }}
{{ hostvars[item]['ansible_port'] | default(22) }}
register: ssh_results
loop: "{{ groups['active'] | difference([inventory_hostname]) }}"
loop_control:
label: "{{ item }} ({{ hostvars[item]['ansible_host'] }}:{{ hostvars[item]['ansible_port'] | default(22) }})"
changed_when: false
failed_when: false
- name: Build SSH reachability summary map
ansible.builtin.set_fact:
ssh_map: >-
{{
ssh_map | default({}) | combine({
item.item: {
'host': hostvars[item.item]['ansible_host'],
'port': hostvars[item.item]['ansible_port'] | default(22),
'rc': item.rc,
'status': 'OK' if item.rc == 0 else 'FAIL'
}
})
}}
loop: "{{ ssh_results.results }}"
loop_control:
label: "{{ item.item }}"
# ---------- Per-host connectivity summary ----------
- name: Display per-host connectivity summary
ansible.builtin.debug:
msg: |
==========================================
CONNECTIVITY SUMMARY: {{ inventory_hostname }}
==========================================
Tailscale:
binary: {{ ts_bin.stdout if ts_bin.stdout | length > 0 else 'not found' }}
backend_state: {{ ts_backend_state }}
first_ip: {{ ts_first_ip }}
Ping matrix (from {{ inventory_hostname }}):
{% for peer, result in (ping_map | default({})).items() %}
{{ peer }} ({{ result.host }}): {{ result.status }}
{% endfor %}
SSH port reachability (from {{ inventory_hostname }}):
{% for peer, result in (ssh_map | default({})).items() %}
{{ peer }} ({{ result.host }}:{{ result.port }}): {{ result.status }}
{% endfor %}
==========================================
# ---------- HTTP endpoint checks (run once from localhost) ----------
- name: Check HTTP endpoints
ansible.builtin.uri:
url: "{{ item.url }}"
method: GET
status_code: [200, 301, 302, 401, 403]
timeout: 10
validate_certs: false
register: http_results
loop: "{{ http_endpoints }}"
loop_control:
label: "{{ item.name }} ({{ item.url }})"
delegate_to: localhost
run_once: true
failed_when: false
- name: Display HTTP endpoint results
ansible.builtin.debug:
msg: |
==========================================
HTTP ENDPOINT RESULTS
==========================================
{% for result in http_results.results %}
{{ result.item.name }} ({{ result.item.url }}):
status: {{ result.status | default('UNREACHABLE') }}
ok: {{ 'YES' if result.status is defined and result.status in [200, 301, 302, 401, 403] else 'NO' }}
{% endfor %}
==========================================
delegate_to: localhost
run_once: true
# ---------- ntfy alert for failed ping peers ----------
- name: Send ntfy alert when peers fail ping
ansible.builtin.uri:
url: "{{ ntfy_url }}"
method: POST
body: |
Host {{ inventory_hostname }} detected {{ failed_ping_peers | length }} unreachable peer(s):
{% for peer in failed_ping_peers %}
- {{ peer }} ({{ hostvars[peer]['ansible_host'] }})
{% endfor %}
Checked at {{ ansible_date_time.iso8601 }}
headers:
Title: "Homelab Network Alert"
Priority: "high"
Tags: "warning,network"
status_code: [200, 204]
delegate_to: localhost
failed_when: false
when: failed_ping_peers | default([]) | length > 0
# ---------- Per-host JSON report ----------
- name: Write per-host JSON connectivity report
ansible.builtin.copy:
content: "{{ {'timestamp': ansible_date_time.iso8601, 'hostname': inventory_hostname, 'tailscale': {'binary': ts_bin.stdout | default('') | trim, 'backend_state': ts_backend_state, 'first_ip': ts_first_ip}, 'ping_matrix': ping_map | default({}), 'ssh_reachability': ssh_map | default({}), 'failed_ping_peers': failed_ping_peers | default([])} | to_nice_json }}"
dest: "{{ report_dir }}/{{ inventory_hostname }}_{{ ansible_date_time.date }}.json"
delegate_to: localhost
changed_when: false