Files
ansible-worker/files/ansible-playbooks/sot-updater.yml
2025-10-24 12:05:26 +03:00

504 lines
18 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
# sot-updater.yml — Read Dev1 & Dev2 firmware and publish to controls → netbox-reporter
# Run: nbplay sot-updater.yml <device>
- name: Read fw on Dev1 + Dev2, publish NetBox custom fields
hosts: all
gather_facts: no
vars:
# Busybox-safe PATH prefix for raw calls on DEV1
pathprefix: "PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; "
# DEV1 credentials for tunnel
dev1_user: "root"
dev1_pass: "wavewave"
# Tunnel target DEV2 behind DEV1
dev2_host: "192.168.1.1"
dev2_port: 22
dev2_ssh_user: "root"
dev2_side_ip: "192.168.1.11/24"
dev1_iface: "br-wan"
# Rabbit journaling defaults
rmq_host: "{{ lookup('env','RMQ_HOST') | default('10.210.12.2', true) }}"
rmq_port: "{{ lookup('env','RMQ_PORT') | default('15672', true) }}"
rmq_user: "{{ lookup('env','RMQ_USER') | default('admin', true) }}"
rmq_pass: "{{ lookup('env','RMQ_PASS') | default('change_me', true) }}"
rmq_vhost: "{{ lookup('env','RMQ_VHOST') | default('app', true) }}"
rmq_exchange: "{{ lookup('env','RMQ_EXCHANGE') | default('controls', true) }}"
control_queue: "{{ lookup('env','CONTROLQUEUE') | default('queue_controls', true) }}"
tasks:
# ───────────────────────── Progress: start ─────────────────────────
- name: "⚙️ Start | Device processing begins (Dev1 phase)"
debug:
msg:
- "Device: {{ inventory_hostname }}"
- "Phase: Dev1 (read banner & publish fw_version)"
# ───────────────────────── DEV1 ─────────────────────────
- name: Dev1 | Read first /etc/banner line containing 'rev'
ansible.builtin.raw: "{{ pathprefix }} cat /etc/banner | grep -i rev | head -n1"
register: dev1_banner
changed_when: false
- name: Dev1 | Debug banner output
debug:
msg:
- "Dev1 banner raw: {{ (dev1_banner.stdout | default('') | trim) | regex_replace('\\n',' ') }}"
- name: Dev1 | Strip trailing timestamp after pipe
delegate_to: localhost
set_fact:
dev1_fw_base: "{{ (dev1_banner.stdout | default('') | trim) | regex_replace('\\s*\\|.*$', '') }}"
- name: Dev1 | Convert '... rev NNNN' → '...-rNNNN'
delegate_to: localhost
set_fact:
dev1_fw_clean: "{{ dev1_fw_base | regex_replace('\\s*[Rr][Ee][Vv]\\.??\\s*([0-9]+)\\s*$', '-r\\1') }}"
- name: Dev1 | Debug normalized fw string to be published
delegate_to: localhost
debug:
msg:
- "Dev1 fw_version (clean): {{ dev1_fw_clean }}"
# Build payload preview for Dev1 (so the next debug is defined)
- name: Dev1 | Build payload preview string
delegate_to: localhost
set_fact:
dev1_payload_preview: >-
{{
{
"properties": {"content_type": "application/json"},
"routing_key": control_queue,
"payload": ({
"inscope_device": (ansible_hostname | default(inventory_hostname)),
"task_name": "custom_field_set",
"task_add1": "fw_version",
"task_result": dev1_fw_clean
} | to_json),
"payload_encoding": "string"
} | to_json
}}
- name: Dev1 | Debug EXACT publish body (payload preview)
when: dev1_payload_preview is defined
delegate_to: localhost
debug:
var: dev1_payload_preview
- name: Publish → controls | custom_field_set fw_version (Dev1)
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'custom_field_set',
'task_add1': 'fw_version',
'task_result': dev1_fw_clean
} | to_json }}"
payload_encoding: "string"
register: dev1_publish_resp
changed_when: false
- name: Dev1 | Debug publish API response
delegate_to: localhost
debug:
msg:
- "Dev1 publish response: {{ dev1_publish_resp.json | default(dev1_publish_resp) }}"
- name: "✅ Progress | Completed Dev1, moving to tunnel & Dev2 phase"
debug:
msg:
- "Device: {{ inventory_hostname }}"
- "Phase switch: Dev1 done → Dev2 (tunnel, auth, read indoor_fwver)"
- name: Refresh ARP 1 on DEV1s LAN (send unsolicited ARP from temporary IP)
ansible.builtin.raw: arping -U -I eth0 192.168.1.11 -c 3
# ───────────────────────── TUNNEL PREP ─────────────────────────
- name: Add temporary IP on DEV1 (tolerate 'File exists')
ansible.builtin.raw: >
{{ pathprefix }}
ip a add {{ dev2_side_ip }} dev {{ dev1_iface }}
register: add_ip
changed_when: add_ip.rc == 0
failed_when: add_ip.rc != 0
and ('File exists' not in (add_ip.stdout | default('')))
and ('File exists' not in (add_ip.stderr | default('')))
- name: Debug | temp IP add result
debug:
msg:
- "Add IP rc={{ add_ip.rc }}"
- "stdout={{ (add_ip.stdout | default('')) | trim }}"
- "stderr={{ (add_ip.stderr | default('')) | trim }}"
- name: Pick a free local TCP port for the tunnel (controller)
delegate_to: localhost
ansible.builtin.shell: |
set -e
pick() {
for i in $(seq 1 25); do
p="$(shuf -i 20000-39999 -n 1)"
if command -v ss >/dev/null 2>&1; then
if ! ss -ltn | awk '{print $4}' | grep -qE "(:|\.)${p}$"; then
echo "$p"; return 0
fi
else
if ! nc -z 127.0.0.1 "$p" >/dev/null 2>&1; then
echo "$p"; return 0
fi
fi
done
return 1
}
pick
register: pick_port
changed_when: false
- name: Debug | chosen local tunnel port
delegate_to: localhost
debug:
msg:
- "Chosen local port: {{ pick_port.stdout | trim }}"
- name: Stop if no free local port was found
ansible.builtin.meta: end_host
when: (pick_port.stdout | trim | length) == 0
- name: Record chosen local port
delegate_to: localhost
set_fact:
_local_port: "{{ pick_port.stdout | trim }}"
- name: Create ControlMaster socket dir (mktemp)
delegate_to: localhost
set_fact:
_ctrl_dir: "{{ lookup('pipe', 'mktemp -d') }}"
- name: Compose ControlMaster socket path
delegate_to: localhost
set_fact:
_ctrl_sock: "{{ _ctrl_dir }}/ssh_tunnel_ctl"
- name: Debug | ControlMaster paths
delegate_to: localhost
debug:
msg:
- "_ctrl_dir={{ _ctrl_dir }}"
- "_ctrl_sock={{ _ctrl_sock }}"
- name: Refresh ARP 1 on DEV1s LAN (send unsolicited ARP from temporary IP)
ansible.builtin.raw: arping -U -I eth0 192.168.1.11 -c 3
- name: Start SSH ControlMaster and forward 127.0.0.1:local → DEV2:22 via DEV1
delegate_to: localhost
ansible.builtin.shell: |
set -e
USER="{{ dev1_user }}"
HOST="{{ ansible_host | default(inventory_hostname) }}"
sshpass -p '{{ dev1_pass }}' ssh -f -N \
-o PreferredAuthentications=password -o PubkeyAuthentication=no \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o NumberOfPasswordPrompts=1 -o ConnectTimeout=30 \
-M -S "{{ _ctrl_sock }}" \
-L "127.0.0.1:{{ _local_port }}:{{ dev2_host }}:{{ dev2_port }}" \
"${USER}@${HOST}"
args: { executable: /bin/bash }
register: start_tunnel
changed_when: true
- name: Debug | tunnel start status
delegate_to: localhost
debug:
msg:
- "Started ControlMaster tunnel (rc={{ start_tunnel.rc | default('n/a') }})"
- "stdout={{ (start_tunnel.stdout | default('')) | trim }}"
- "stderr={{ (start_tunnel.stderr | default('')) | trim }}"
- name: Probe TCP reachability to DEV2 through the tunnel
delegate_to: localhost
ansible.builtin.shell: "nc -z -w5 127.0.0.1 {{ _local_port }}"
register: nc_probe
changed_when: false
ignore_errors: true
- name: Debug | tunnel probe result
delegate_to: localhost
debug:
msg:
- "Tunnel TCP probe rc={{ nc_probe.rc }} (0=OK)"
- "stdout={{ (nc_probe.stdout | default('')) | trim }}"
- "stderr={{ (nc_probe.stderr | default('')) | trim }}"
- name: Stop if tunnel TCP probe failed
ansible.builtin.meta: end_host
when: nc_probe.rc != 0
- name: "✅ Progress | Tunnel OK, proceeding to Dev2 auth and reads"
debug:
msg:
- "Device: {{ inventory_hostname }}"
- "Phase: Dev2 (auth selection, firmux/banner read)"
- name: Refresh ARP 1 on DEV1s LAN (send unsolicited ARP from temporary IP)
ansible.builtin.raw: arping -U -I eth0 192.168.1.11 -c 3
# ───────────────────────── DEV2 AUTH PICK (as per update-indoor) ─────────────────────────
- name: Try DEV2 login with 'basicpass' (root)
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f basicpass ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=30 \
-p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1
register: dev2_try_basicpass
changed_when: false
ignore_errors: true
- name: Debug | basicpass try rc
delegate_to: localhost
debug:
msg: "DEV2 'basicpass' try rc={{ dev2_try_basicpass.rc }} (0=OK)"
- name: Select 'basicpass' if previous login succeeded
when: dev2_try_basicpass.rc == 0
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "basicpass"
changed_when: false
- name: Try DEV2 login with 'basicpass2' (only if first failed)
when: dev2_passfile_used is not defined
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f basicpass2 ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=30 \
-p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1
register: dev2_try_basicpass2
changed_when: false
ignore_errors: true
- name: Debug | basicpass2 try rc
delegate_to: localhost
debug:
msg: "DEV2 'basicpass2' try rc={{ dev2_try_basicpass2.rc | default('n/a') }} (0=OK)"
- name: Select 'basicpass2' if previous login succeeded
when: dev2_passfile_used is not defined and dev2_try_basicpass2.rc == 0
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "basicpass2"
changed_when: false
- name: Mark DEV2 auth as NONE if both attempts failed
when: dev2_passfile_used is not defined
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "NONE"
changed_when: false
- name: Debug | selected passfile
delegate_to: localhost
debug:
msg: "DEV2 passfile used: {{ dev2_passfile_used }}"
- name: Refresh ARP 1 on DEV1s LAN (send unsolicited ARP from temporary IP)
ansible.builtin.raw: arping -U -I eth0 192.168.1.11 -c 3
# ───────────────────────── DEV2 READS ─────────────────────────
- name: Dev2 | Read /usr/lib/release/firmux (if present)
when: dev2_passfile_used != "NONE"
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=30 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"cat /usr/lib/release/firmux 2>/dev/null || true"
args: { executable: /bin/bash }
register: dev2_firmux
changed_when: false
ignore_errors: true
### added by pavel
- name: Debug output firmux variable
delegate_to: localhost
debug:
msg: "firmux caught: {{ dev2_firmux.stdout }}"
# Dev2: firmux-only selection (prefer a line that has "rev NNNN")
- name: Dev2 | Prepare firmux lines list
delegate_to: localhost
set_fact:
_firmux_lines: "{{ (dev2_firmux.stdout | default('') | regex_replace('\r','')) | split('\n') | map('trim') | list }}"
- name: Dev2 | Select line from firmux (prefer '... rev NNNN')
delegate_to: localhost
set_fact:
indoor_fw_raw: >-
{{
(
_firmux_lines
| select('match', '(?i).*\\brev\\s*\\d+.*')
| list
)[0]
if (
_firmux_lines
| select('match', '(?i).*\\brev\\s*\\d+.*')
| list
| length
) > 0
else
((_firmux_lines | select('truthy') | list )[0] | default(''))
}}
- name: Dev2 | Debug chosen line from firmux
delegate_to: localhost
debug:
msg: "Chosen indoor_fw_raw='{{ indoor_fw_raw }}'"
# Final value for NetBox: keep exactly what firmux says (no '-r' conversion)
- name: Dev2 | Final indoor fw string (no transformation)
delegate_to: localhost
set_fact:
indoor_fw_norm: "{{ indoor_fw_raw | trim }}"
# Build payload preview (exact string) — single, corrected task
- name: Dev2 | Build payload preview string
when:
- dev2_passfile_used is defined
- dev2_passfile_used != "NONE"
- indoor_fw_norm is defined
- (indoor_fw_norm | length) > 0
delegate_to: localhost
set_fact:
dev2_payload_preview: >-
{{
{
"properties": {"content_type": "application/json"},
"routing_key": control_queue,
"payload": ({
"inscope_device": (ansible_hostname | default(inventory_hostname)),
"task_name": "custom_field_set",
"task_add1": "indoor_fwver",
"task_result": indoor_fw_norm
} | to_json),
"payload_encoding": "string"
} | to_json
}}
- name: Dev2 | Debug EXACT publish body (payload preview)
when:
- dev2_passfile_used != "NONE"
- dev2_payload_preview is defined
delegate_to: localhost
debug:
var: dev2_payload_preview
- name: Publish → controls | custom_field_set indoor_fwver (Dev2)
when:
- dev2_passfile_used != "NONE"
- indoor_fw_norm is defined
- (indoor_fw_norm | length) > 0
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'custom_field_set',
'task_add1': 'indoor_fwver',
'task_result': indoor_fw_norm
} | to_json }}"
payload_encoding: "string"
register: dev2_publish_resp
changed_when: false
- name: Dev2 | Debug publish API response
when: dev2_publish_resp is defined
delegate_to: localhost
debug:
msg:
- "Dev2 publish response: {{ dev2_publish_resp.json | default(dev2_publish_resp) }}"
post_tasks:
- name: "✅ Progress | Finishing up (cleanup)"
debug:
msg:
- "Cleanup phase: closing ControlMaster & removing temp IP"
- name: Close SSH ControlMaster (best-effort)
delegate_to: localhost
ansible.builtin.shell: "ssh -S '{{ _ctrl_sock | default('/dev/null') }}' -O exit 2>/dev/null || true"
changed_when: false
ignore_errors: true
- name: Remove tunnel control dir (best-effort)
delegate_to: localhost
ansible.builtin.file:
path: "{{ _ctrl_dir | default('/tmp/none') }}"
state: absent
ignore_errors: true
- name: Remove temporary IP on DEV1 (tolerate 'Cannot assign requested address')
ansible.builtin.raw: >
{{ pathprefix }}
ip a del {{ dev2_side_ip }} dev {{ dev1_iface }}
register: del_ip
changed_when: del_ip.rc == 0
failed_when: del_ip.rc != 0
and ('Cannot assign requested address' not in (del_ip.stdout | default('')))
and ('Cannot assign requested address' not in (del_ip.stderr | default('')))
- name: Debug | temp IP del result
debug:
msg:
- "Del IP rc={{ del_ip.rc }}"
- "stdout={{ (del_ip.stdout | default('')) | trim }}"
- "stderr={{ (del_ip.stderr | default('')) | trim }}"
- name: "✅ Progress | Completed device processing"
debug:
msg:
- "Device: {{ inventory_hostname }}"
- "Status: DONE (Dev1+Dev2 processed)"