235 lines
8.4 KiB
YAML
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
|