From 5b63184aa3d5fed0253a4076b03e35a04d342ae9 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 24 Oct 2025 15:24:34 +0300 Subject: [PATCH] 1205 --- files/ansible-playbooks/sot-updater.yml | 702 ++++++++++++------------ 1 file changed, 351 insertions(+), 351 deletions(-) diff --git a/files/ansible-playbooks/sot-updater.yml b/files/ansible-playbooks/sot-updater.yml index ae1141e..934c21f 100644 --- a/files/ansible-playbooks/sot-updater.yml +++ b/files/ansible-playbooks/sot-updater.yml @@ -2,26 +2,41 @@ # sot-updater.yml — Read Dev1 & Dev2 firmware and publish to controls → netbox-reporter # Run: nbplay sot-updater.yml -- name: Read fw on Dev1 + Dev2, publish NetBox custom fields +- name: Read fw on Dev1 + Dev2, publish NetBox custom fields (full base, AIRPINGs, soft-fail telemetry) hosts: all gather_facts: no vars: - # Busybox-safe PATH prefix for raw calls on DEV1 + # Busybox-safe PATH prefix for *all* raw calls on DEV1 pathprefix: "PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; " - # DEV1 credentials for tunnel + # DEV1 credentials (controller-side) dev1_user: "root" dev1_pass: "wavewave" + ssh_timeout: 30 - # Tunnel target DEV2 behind DEV1 + # 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" + dev2_passfiles: [ "basicpass", "basicpass2" ] - # Rabbit journaling defaults + # DEV1 side addressing to reach DEV2 + dev2_side_ip: "192.168.1.11/24" # IP we add/remove on DEV1 + dev1_iface: "br-wan" # DEV1 interface where IP is added + arping_iface: "eth0" # iface used for ARP refreshes (as per update-indoor) + dev2_side_ip_addr: "{{ dev2_side_ip.split('/')[0] }}" + + # Controller SSH common opts + ssh_opts_common: >- + -o PreferredAuthentications=password + -o PubkeyAuthentication=no + -o StrictHostKeyChecking=no + -o UserKnownHostsFile=/dev/null + -o NumberOfPasswordPrompts=1 + -o ConnectTimeout=30 + + # Rabbit (controls) 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) }}" @@ -32,63 +47,235 @@ tasks: # ───────────────────────── Progress: start ───────────────────────── - - name: "⚙️ Start | Device processing begins (Dev1 phase)" + - name: "⚙️ Start | Dev1 banner → tunnel/auth → Dev2 firmux (telemetry mode)" debug: msg: - "Device: {{ inventory_hostname }}" - - "Phase: Dev1 (read banner & publish fw_version)" + - "Mode: report-only (soft-fail; publish journals on failures)" - # ───────────────────────── 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 + # ---------------------------- Temp IP on DEV1 ---------------------------- + - name: Add temporary IP on DEV1 (tolerate 'File exists'; soft-fail) + ansible.builtin.raw: > + {{ pathprefix }} + ip a add {{ dev2_side_ip }} dev {{ dev1_iface }} + register: add_ip + changed_when: add_ip.rc == 0 + failed_when: false + ignore_errors: true + + - name: Soft-report if temp IP add failed (not just 'File exists') + when: add_ip.rc is defined and add_ip.rc != 0 and + ('File exists' not in (add_ip.stdout | default(''))) and + ('File exists' not in (add_ip.stderr | default(''))) + 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': 'journal_entry', + 'task_result': 'temp_ip_add_failed', + 'task_add1': 'rc=' ~ (add_ip.rc | string), + 'task_add2': (add_ip.stderr | default(add_ip.stdout) | trim)[:512] + } | to_json }}" + payload_encoding: "string" + + - name: Debug temp IP add + debug: + msg: + - "rc={{ add_ip.rc | default('') }}" + - "stdout={{ (add_ip.stdout | default('')) | trim }}" + - "stderr={{ (add_ip.stderr | default('')) | trim }}" + + # ---------------------------- AIRPING #1 (early refresh) ---------------------------- + - name: Refresh ARP #1 on DEV1 LAN (unsolicited) + ansible.builtin.raw: > + {{ pathprefix }} + arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3 + register: arping1 + changed_when: false + failed_when: false + ignore_errors: true + + # ---------------------------- Local tunnel prep ---------------------------- + - 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: Dev1 | Debug banner output - debug: - msg: - - "Dev1 banner raw: {{ (dev1_banner.stdout | default('') | trim) | regex_replace('\\n',' ') }}" - - - name: Dev1 | Strip trailing timestamp after pipe + - name: Record chosen local port & ControlMaster path (or note failure) delegate_to: localhost set_fact: - dev1_fw_base: "{{ (dev1_banner.stdout | default('') | trim) | regex_replace('\\s*\\|.*$', '') }}" + _local_port: "{{ (pick_port.stdout | default('') | trim) }}" + _ctrl_dir: "{{ lookup('ansible.builtin.pipe', 'mktemp -d') }}" + _ctrl_sock: "{{ _ctrl_dir }}/ssh_tunnel_ctl" - - name: Dev1 | Convert '... rev NNNN' → '...-rNNNN' + - name: Journal if no free local port was found (continue anyway) + when: _local_port | 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': 'journal_entry', + 'task_result': 'tunnel_port_pick_failed' + } | to_json }}" + payload_encoding: "string" + + # ---------------------------- AIRPING #2 (before tunnel) ---------------------------- + - name: Refresh ARP #2 on DEV1 LAN (pre-tunnel) + when: _local_port | length > 0 + ansible.builtin.raw: > + {{ pathprefix }} + arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3 + register: arping2 + changed_when: false + failed_when: false + ignore_errors: true + + # ---------------------------- Start tunnel via DEV1 ---------------------------- + - name: Start SSH ControlMaster and forward 127.0.0.1:local → DEV2:22 via DEV1 + when: _local_port | length > 0 + delegate_to: localhost + ansible.builtin.shell: | + set -e + USER="{{ dev1_user }}" + HOST="{{ ansible_host | default(inventory_hostname) }}" + sshpass -p '{{ dev1_pass }}' ssh -f -N {{ ssh_opts_common }} \ + -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 + failed_when: false + ignore_errors: true + + - name: Probe TCP reachability to DEV2 through the tunnel (nc) + when: _local_port | length > 0 + delegate_to: localhost + ansible.builtin.shell: "nc -z -w5 127.0.0.1 {{ _local_port }}" + register: nc_probe + changed_when: false + failed_when: false + ignore_errors: true + + - name: Set tunnel_ok fact delegate_to: localhost set_fact: - dev1_fw_clean: "{{ dev1_fw_base | regex_replace('\\s*[Rr][Ee][Vv]\\.??\\s*([0-9]+)\\s*$', '-r\\1') }}" + tunnel_ok: "{{ (_local_port | length > 0) and (nc_probe.rc is defined) and (nc_probe.rc == 0) }}" - - name: Dev1 | Debug normalized fw string to be published + - name: Journal if tunnel probe failed + when: not tunnel_ok delegate_to: localhost - debug: - msg: - - "Dev1 fw_version (clean): {{ dev1_fw_clean }}" + 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': 'journal_entry', + 'task_result': 'tunnel_probe_failed', + 'task_add1': 'port=' ~ (_local_port | default('n/a')) + } | to_json }}" + payload_encoding: "string" - # Build payload preview for Dev1 (so the next debug is defined) - - name: Dev1 | Build payload preview string + # ───────────────────────── DEV1: banner → fw_version ───────────────────────── + - name: Dev1 | Probe banner via SSH from controller (classic extraction) + delegate_to: localhost + ansible.builtin.shell: | + set -e + USER="{{ dev1_user }}" + HOST="{{ ansible_host | default(inventory_hostname) }}" + sshpass -p '{{ dev1_pass }}' \ + ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -o ConnectTimeout={{ ssh_timeout }} \ + "${USER}@${HOST}" \ + "PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; cat /etc/banner | grep -i rev | head -n1" + register: dev1_banner + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Normalize banner → X.X.X-rYYYY delegate_to: localhost set_fact: - dev1_payload_preview: >- + dev1_fw_clean: >- {{ - { - "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 + ( + (dev1_banner.stdout | default('') | trim ) + | regex_replace('\\s*\\|.*$', '') # drop ' | timestamp' + | regex_replace('(?i)\\s*rev\\.?\\s*([0-9]+)\\s*$', '-r\\1') + | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+-r[0-9]+') + ) | default('', true) }} - - name: Dev1 | Debug EXACT publish body (payload preview) - when: dev1_payload_preview is defined + - name: Journal if DEV1 normalization failed (no core) + when: (dev1_fw_clean | default('') | length) == 0 delegate_to: localhost - debug: - var: dev1_payload_preview + 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': 'journal_entry', + 'task_result': 'dev1_banner_parse_failed', + 'task_add1': (dev1_banner.stdout | default('NO_STDOUT') | trim)[:256] + } | to_json }}" + payload_encoding: "string" - name: Publish → controls | custom_field_set fw_version (Dev1) delegate_to: localhost @@ -108,193 +295,15 @@ 'inscope_device': (ansible_hostname | default(inventory_hostname)), 'task_name': 'custom_field_set', 'task_add1': 'fw_version', - 'task_result': dev1_fw_clean + 'task_result': (dev1_fw_clean if (dev1_fw_clean | length) > 0 else 'unavailable') } | 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) ───────────────────────── + # ───────────────────────── DEV2 auth pick ───────────────────────── - name: Try DEV2 login with 'basicpass' (root) + when: tunnel_ok | default(false) delegate_to: localhost ansible.builtin.shell: | set -e @@ -307,22 +316,17 @@ -p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1 register: dev2_try_basicpass changed_when: false + failed_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 + when: tunnel_ok | default(false) and 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 + when: tunnel_ok | default(false) and dev2_passfile_used is not defined delegate_to: localhost ansible.builtin.shell: | set -e @@ -335,49 +339,57 @@ -p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1 register: dev2_try_basicpass2 changed_when: false + failed_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 + when: tunnel_ok | default(false) and (dev2_passfile_used is not defined) and (dev2_try_basicpass2.rc | default(1)) == 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 + when: tunnel_ok | default(false) and (dev2_passfile_used is not defined) delegate_to: localhost ansible.builtin.set_fact: dev2_passfile_used: "NONE" - changed_when: false - - name: Debug | selected passfile + - name: Journal if DEV2 auth failed + when: tunnel_ok | default(false) and (dev2_passfile_used == 'NONE') delegate_to: localhost - debug: - msg: "DEV2 passfile used: {{ dev2_passfile_used }}" + 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': 'journal_entry', + 'task_result': 'dev2_auth_failed' + } | to_json }}" + payload_encoding: "string" - - name: Dev2 | ARP ping the target (from Dev1) to confirm who is at {{ dev2_host }} + # ---------------------------- AIRPING #3 (post-tunnel stabilizer) ---------------------------- + - name: Refresh ARP #3 on DEV1 LAN (post-tunnel) + when: tunnel_ok | default(false) ansible.builtin.raw: > {{ pathprefix }} - arping -c 1 -w 2 -I {{ dev1_iface }} {{ dev2_host }} 2>&1 || true - register: dev2_arping + arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3 + register: arping3 changed_when: false + failed_when: false + ignore_errors: true - - 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" + # ───────────────────────── DEV2: firmux → indoor_fwver ───────────────────────── + - name: Dev2 | Read /usr/lib/release/firmux (if authenticated) + when: tunnel_ok | default(false) and dev2_passfile_used is defined and dev2_passfile_used != 'NONE' delegate_to: localhost ansible.builtin.shell: | set -e @@ -392,90 +404,54 @@ args: { executable: /bin/bash } register: dev2_firmux changed_when: false + failed_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 + - name: Dev2 | Choose firmux line (prefer containing 'rev NNNN') + when: tunnel_ok | default(false) 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: >- + dev2_line: >- {{ - ( - _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('')) + (_firmux_lines | select('match','(?i).*\\brev\\s*\\d+.*') | list | first) + | default((_firmux_lines | select('truthy') | list | first) | 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) + - name: Dev2 | Normalize to EXACT 'X.X.X rev YYYY' (lowercase 'rev', single space) + when: tunnel_ok | default(false) delegate_to: localhost set_fact: - indoor_fw_norm: "{{ indoor_fw_raw | trim }}" + dev2_line_clean: "{{ (dev2_line | default('') | trim) | regex_replace('\\s*\\|.*$', '') }}" + dev2_ver: "{{ dev2_line_clean | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+') | default('') }}" + dev2_rev_token: "{{ dev2_line_clean | regex_search('(?i)\\brev\\.?\\s*[0-9]+') | default('') }}" + dev2_rev_num: "{{ dev2_rev_token | regex_replace('(?i).*\\brev\\.?\\s*', '') }}" + indoor_fw_norm: "{{ (dev2_ver ~ ' rev ' ~ dev2_rev_num) if (dev2_ver|length>0 and dev2_rev_num|length>0) else '' }}" - # 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 + - name: Journal if DEV2 normalization failed (no version or rev) + when: tunnel_ok | default(false) and (indoor_fw_norm | default('') | 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 + 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': 'journal_entry', + 'task_result': 'dev2_firmux_parse_failed', + 'task_add1': (dev2_firmux.stdout | default('NO_STDOUT') | trim)[:256] + } | to_json }}" + payload_encoding: "string" - 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" @@ -493,29 +469,35 @@ 'inscope_device': (ansible_hostname | default(inventory_hostname)), 'task_name': 'custom_field_set', 'task_add1': 'indoor_fwver', - 'task_result': indoor_fw_norm + 'task_result': ( + indoor_fw_norm if (tunnel_ok | default(false)) and (indoor_fw_norm | default('') | length) > 0 + else ('unreachable' if not (tunnel_ok | default(false)) else 'unavailable') + ) } | 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)" + - name: "✅ Progress | Cleanup start (close tunnel & remove temp IP)" debug: - msg: - - "Cleanup phase: closing ControlMaster & removing temp IP" + msg: "Closing ControlMaster and removing temporary IP" + + # ---------------------------- AIRPING #4 (pre-cleanup) ---------------------------- + - name: Refresh ARP #4 on DEV1 LAN (pre-cleanup) + ansible.builtin.raw: > + {{ pathprefix }} + arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3 + register: arping4 + changed_when: false + failed_when: false + ignore_errors: true - 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 + failed_when: false ignore_errors: true - name: Remove tunnel control dir (best-effort) @@ -525,25 +507,43 @@ state: absent ignore_errors: true - - name: Remove temporary IP on DEV1 (tolerate 'Cannot assign requested address') + - name: Remove temporary IP on DEV1 (tolerate 'Cannot assign requested address'; soft-fail) 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(''))) + failed_when: false + ignore_errors: true - - 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: Journal if temp IP delete failed (not just 'Cannot assign requested address') + when: del_ip.rc is defined and 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(''))) + 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': 'journal_entry', + 'task_result': 'temp_ip_del_failed', + 'task_add1': 'rc=' ~ (del_ip.rc | string), + 'task_add2': (del_ip.stderr | default(del_ip.stdout) | trim)[:512] + } | to_json }}" + payload_encoding: "string" - - name: "✅ Progress | Completed device processing" + - name: "✅ Completed | Device processed (Dev1+Dev2)." debug: msg: - "Device: {{ inventory_hostname }}" - - "Status: DONE (Dev1+Dev2 processed)" + - "Status: DONE (report-only)"