Sanitized mirror from private repository - 2026-04-18 11:19:59 UTC
This commit is contained in:
234
ansible/automation/playbooks/network_connectivity.yml
Normal file
234
ansible/automation/playbooks/network_connectivity.yml
Normal file
@@ -0,0 +1,234 @@
|
||||
---
|
||||
# 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
|
||||
Reference in New Issue
Block a user