Files
ansible-worker/files/ansible-playbooks/sot-updater.yml
2025-10-24 13:41:18 +03:00

550 lines
20 KiB
YAML

---
# 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: Dev2 | ARP ping the target (from Dev1) to confirm who is at {{ dev2_host }}
ansible.builtin.raw: >
{{ pathprefix }}
arping -c 1 -w 2 -I {{ dev1_iface }} {{ dev2_host }} 2>&1 || true
register: dev2_arping
changed_when: false
- name: Debug | arping summary (MAC & replies)
debug:
msg:
- "arping rc={{ dev2_arping.rc }}"
- "arping stdout={{ (dev2_arping.stdout | default('')) | trim }}"
- "arping stderr={{ (dev2_arping.stderr | default('')) | trim }}"
# ───────────────────────── 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: Dev2 | ARP ping the target (from Dev1) to confirm who is at {{ dev2_host }}
ansible.builtin.raw: >
{{ pathprefix }}
arping -c 1 -w 2 -I {{ dev1_iface }} {{ dev2_host }} 2>&1 || true
register: dev2_arping
changed_when: false
- name: Debug | arping summary (MAC & replies)
debug:
msg:
- "arping rc={{ dev2_arping.rc }}"
- "arping stdout={{ (dev2_arping.stdout | default('')) | trim }}"
- "arping stderr={{ (dev2_arping.stderr | default('')) | trim }}"
- 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: Dev2 | ARP ping the target (from Dev1) to confirm who is at {{ dev2_host }}
ansible.builtin.raw: >
{{ pathprefix }}
arping -c 1 -w 2 -I {{ dev1_iface }} {{ dev2_host }} 2>&1 || true
register: dev2_arping
changed_when: false
- name: Debug | arping summary (MAC & replies)
debug:
msg:
- "arping rc={{ dev2_arping.rc }}"
- "arping stdout={{ (dev2_arping.stdout | default('')) | trim }}"
- "arping stderr={{ (dev2_arping.stderr | default('')) | trim }}"
# ───────────────────────── 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: Dev2 | ARP ping the target (from Dev1) to confirm who is at {{ dev2_host }}
ansible.builtin.raw: >
{{ pathprefix }}
arping -c 1 -w 2 -I {{ dev1_iface }} {{ dev2_host }} 2>&1 || true
register: dev2_arping
changed_when: false
- name: Debug | arping summary (MAC & replies)
debug:
msg:
- "arping rc={{ dev2_arping.rc }}"
- "arping stdout={{ (dev2_arping.stdout | default('')) | trim }}"
- "arping stderr={{ (dev2_arping.stderr | default('')) | trim }}"
# ───────────────────────── 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)"