From acf2fff6f50fd912003a68ba2396957f926216d2 Mon Sep 17 00:00:00 2001 From: pavel Date: Sat, 25 Oct 2025 04:14:01 +0300 Subject: [PATCH] 0413 --- files/ansible-playbooks/sot-updater.yml | 517 +++++------------------- 1 file changed, 95 insertions(+), 422 deletions(-) diff --git a/files/ansible-playbooks/sot-updater.yml b/files/ansible-playbooks/sot-updater.yml index 02ffe72..9244843 100644 --- a/files/ansible-playbooks/sot-updater.yml +++ b/files/ansible-playbooks/sot-updater.yml @@ -1,33 +1,26 @@ --- -# sot-updater.yml — Read Dev1 & Dev2 firmware and publish to controls → netbox-reporter -# Run: nbplay sot-updater.yml +# sot-updater.yml — Read fw on Dev1 + Dev2, publish to NetBox via Rabbit (telemetry mode) - 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 *all* raw calls on DEV1 pathprefix: "PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; " - - # DEV1 credentials (controller-side) dev1_user: "root" dev1_pass: "wavewave" ssh_timeout: 30 - # DEV2 behind DEV1 dev2_host: "192.168.1.1" dev2_port: 22 dev2_ssh_user: "root" dev2_passfiles: [ "basicpass", "basicpass2" ] - # 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 + dev2_side_ip: "192.168.1.11/24" + dev1_iface: "br-wan" + arping_iface: "eth0" dev2_side_ip_addr: "{{ dev2_side_ip.split('/')[0] }}" - # Controller SSH common opts ssh_opts_common: >- -o PreferredAuthentications=password -o PubkeyAuthentication=no @@ -36,7 +29,6 @@ -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) }}" @@ -46,154 +38,59 @@ control_queue: "{{ lookup('env','CONTROLQUEUE') | default('queue_controls', true) }}" tasks: - # ───────────────────────── Progress: start ───────────────────────── - name: "⚙️ Start | Dev1 banner → tunnel/auth → Dev2 firmux (telemetry mode)" debug: msg: - "Device: {{ inventory_hostname }}" - "Mode: report-only (soft-fail; publish journals on failures)" - # ---------------------------- 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 }} + # ----------------------- Temp IP on DEV1 ----------------------- + - name: Add temporary IP on DEV1 + 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 + - name: Refresh ARP #1 + raw: "{{ pathprefix }} arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3" failed_when: false ignore_errors: true - # ---------------------------- Local tunnel prep ---------------------------- - - name: Pick a free local TCP port for the tunnel (controller) + # ----------------------- Local tunnel prep ----------------------- + - name: Pick a free local TCP port for the tunnel 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 + shell: | + for i in $(seq 1 50); do + p="$(shuf -i 20000-39999 -n 1)" + ss -ltn | awk '{print $4}' | grep -qE "(:|\.)${p}$" || { echo $p; exit 0; } + done + exit 1 register: pick_port - changed_when: false + failed_when: false - # ***** FIXED: split mktemp + sock path ***** - - name: Record chosen local port + - name: Record chosen port and create control dir + delegate_to: localhost + shell: "mktemp -d" + register: mktemp_dir + failed_when: false + + - name: Set facts for tunnel paths delegate_to: localhost set_fact: _local_port: "{{ (pick_port.stdout | default('') | trim) }}" - - - name: Create ControlMaster socket dir (mktemp -d) - delegate_to: localhost - ansible.builtin.shell: "mktemp -d" - register: mktemp_dir - changed_when: false - failed_when: false - ignore_errors: true - - - name: Set _ctrl_dir fact (from mktemp) or leave empty - delegate_to: localhost - set_fact: _ctrl_dir: "{{ (mktemp_dir.stdout | default('') | trim) }}" + _ctrl_sock: "{{ (mktemp_dir.stdout | default('') | trim) }}/ssh_tunnel_ctl" - - name: Journal if mktemp failed (no ctrl_dir) - when: _ctrl_dir | 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': 'mktemp_dir_failed', - 'task_add1': (mktemp_dir.stderr | default(mktemp_dir.stdout) | trim)[:256] - } | to_json }}" - payload_encoding: "string" - - - name: Compose ControlMaster socket path (only if we have a dir) - when: _ctrl_dir | length > 0 - delegate_to: localhost - set_fact: - _ctrl_sock: "{{ _ctrl_dir }}/ssh_tunnel_ctl" - - # ---------------------------- 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 + # ----------------------- AIRPING #2 ----------------------- + - name: Refresh ARP #2 + raw: "{{ pathprefix }} arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3" 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 + # ----------------------- Start tunnel ----------------------- + - name: Start SSH tunnel via DEV1 delegate_to: localhost - ansible.builtin.shell: | + shell: | set -e USER="{{ dev1_user }}" HOST="{{ ansible_host | default(inventory_hostname) }}" @@ -202,113 +99,45 @@ -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 + - name: Probe tunnel delegate_to: localhost - ansible.builtin.shell: "nc -z -w5 127.0.0.1 {{ _local_port }}" + 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 + - name: Set tunnel_ok delegate_to: localhost set_fact: - tunnel_ok: "{{ (_local_port | length > 0) and (nc_probe.rc is defined) and (nc_probe.rc == 0) }}" + tunnel_ok: "{{ nc_probe.rc == 0 }}" - - name: Journal if tunnel probe failed - when: not tunnel_ok + # ----------------------- DEV1 banner ----------------------- + - name: Dev1 | Probe banner 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_probe_failed', - 'task_add1': 'port=' ~ (_local_port | default('n/a')) - } | to_json }}" - payload_encoding: "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" + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host | default(inventory_hostname) }}" \ + "cat /etc/banner | grep -i rev | head -n1" register: dev1_banner - changed_when: false failed_when: false ignore_errors: true - # ***** FIXED: stepwise, flag-safe normalization for DEV1 ***** - - name: Dev1 | Debug raw banner - when: dev1_banner is defined - delegate_to: localhost - debug: - msg: - - "DEV1 banner raw: {{ (dev1_banner.stdout | default('') | trim) | regex_replace('\n',' ') }}" - - - name: Dev1 | Strip trailing ' | ...' + - name: Dev1 | Normalize banner (X.X.X-rYYYY) delegate_to: localhost set_fact: - dev1_no_ts: "{{ (dev1_banner.stdout | default('') | trim) | regex_replace('\\|.*$', '') }}" + dev1_fw_clean: >- + {{ + (dev1_banner.stdout | default('') | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+-r[0-9]+')) + | default('', true) + | trim + }} - - name: Dev1 | Convert 'rev YYYY' → '-rYYYY' + - name: Publish Dev1 fw_version delegate_to: localhost - set_fact: - dev1_rev_dash: "{{ dev1_no_ts | regex_replace('rev\\.?\\s*([0-9]+)\\s*$', '-r\\1', ignorecase=True) }}" - - - name: Dev1 | Extract X.X.X-rYYYY core - delegate_to: localhost - set_fact: - dev1_fw_clean: "{{ dev1_rev_dash | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+-r[0-9]+') | default('', true) }}" - - - name: Journal if DEV1 normalization failed (no core) - when: (dev1_fw_clean | default('') | 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': '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 - ansible.builtin.uri: + uri: url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" method: POST user: "{{ rmq_user }}" @@ -324,183 +153,67 @@ 'inscope_device': (ansible_hostname | default(inventory_hostname)), 'task_name': 'custom_field_set', 'task_add1': 'fw_version', - 'task_result': (dev1_fw_clean if (dev1_fw_clean | length) > 0 else 'unavailable') + '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 - # ───────────────────────── DEV2 auth pick ───────────────────────── - - name: Try DEV2 login with 'basicpass' (root) + # ----------------------- DEV2 auth ----------------------- + - name: Try DEV2 login when: tunnel_ok | default(false) 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 + shell: | + for f in basicpass basicpass2; do + PORT="{{ _local_port }}" + if sshpass -f "$f" ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -p "$PORT" root@127.0.0.1 "echo ok" >/dev/null 2>&1; then + echo "$f"; exit 0 + fi + done + exit 1 + register: dev2_auth failed_when: false ignore_errors: true - - name: Select 'basicpass' if previous login succeeded - when: tunnel_ok | default(false) and dev2_try_basicpass.rc == 0 + - name: Set dev2_passfile_used delegate_to: localhost - ansible.builtin.set_fact: - dev2_passfile_used: "basicpass" + set_fact: + dev2_passfile_used: "{{ (dev2_auth.rc == 0) | ternary(dev2_auth.stdout | trim, 'NONE') }}" - - name: Try DEV2 login with 'basicpass2' (only if first failed) - when: tunnel_ok | default(false) and 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 + # ----------------------- AIRPING #3 ----------------------- + - name: Refresh ARP #3 + raw: "{{ pathprefix }} arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3" failed_when: false ignore_errors: true - - name: Select 'basicpass2' if previous login succeeded - when: tunnel_ok | default(false) and (dev2_passfile_used is not defined) and (dev2_try_basicpass2.rc | default(1)) == 0 + # ----------------------- DEV2 firmux (simplified, literal) ----------------------- + - name: Dev2 | Read /usr/lib/release/firmux + when: tunnel_ok | default(false) and dev2_passfile_used != 'NONE' delegate_to: localhost - ansible.builtin.set_fact: - dev2_passfile_used: "basicpass2" - - - name: Mark DEV2 auth as NONE if both attempts failed - when: tunnel_ok | default(false) and (dev2_passfile_used is not defined) - delegate_to: localhost - ansible.builtin.set_fact: - dev2_passfile_used: "NONE" - - - name: Journal if DEV2 auth failed - when: tunnel_ok | default(false) and (dev2_passfile_used == 'NONE') - 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': 'dev2_auth_failed' - } | to_json }}" - payload_encoding: "string" - - # ---------------------------- AIRPING #3 (post-tunnel stabilizer) ---------------------------- - - name: Refresh ARP #3 on DEV1 LAN (post-tunnel) - when: tunnel_ok | default(false) - ansible.builtin.raw: > - {{ pathprefix }} - arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3 - register: arping3 - changed_when: false - failed_when: false - ignore_errors: true - - # ───────────────────────── 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 + shell: | 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 \ + -o ConnectTimeout=15 \ -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 failed_when: false ignore_errors: true - - name: Dev2 | Debug raw firmux - when: dev2_firmux is defined - delegate_to: localhost - debug: - msg: - - "DEV2 firmux raw: {{ (dev2_firmux.stdout | default('') | trim) | regex_replace('\n',' | ') }}" - - - name: Dev2 | Build firmux lines list + - name: Dev2 | Extract firmware version line delegate_to: localhost set_fact: - _firmux_lines: "{{ (dev2_firmux.stdout | default('') | regex_replace('\r','')) | split('\n') | map('trim') | list }}" - - - name: Dev2 | Choose firmux line (prefer containing 'rev NNNN') - delegate_to: localhost - set_fact: - dev2_line: >- + indoor_fw_norm: >- {{ - (_firmux_lines | select('match','(?i).*\\brev\\s*\\d+.*') | list | first) - | default((_firmux_lines | select('truthy') | list | first) | default('')) + (dev2_firmux.stdout | default('') | regex_search('.*\\brev\\s*[0-9]+.*')) + | default('', true) + | trim }} - # ***** FIXED: ensure dev2_line_clean is defined before use, stepwise extraction ***** - - name: Dev2 | Strip trailing pipes and whitespace + - name: Publish Dev2 indoor_fwver delegate_to: localhost - set_fact: - dev2_line_clean: "{{ (dev2_line | default('') | trim) | regex_replace('\\|.*$', '') }}" - - - name: Dev2 | Extract version (X.X.X) and rev number - delegate_to: localhost - set_fact: - dev2_ver: "{{ dev2_line_clean | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+') | default('', true) }}" - dev2_rev_token: "{{ dev2_line_clean | regex_search('(?i)\\brev\\.?\\s*[0-9]+') | default('', true) }}" - dev2_rev_num: "{{ (dev2_rev_token | regex_replace('(?i).*\\brev\\.?\\s*', '')) if (dev2_rev_token | length) > 0 else '' }}" - - - name: Dev2 | Build EXACT 'X.X.X rev YYYY' (lowercase 'rev', single space) - delegate_to: localhost - set_fact: - indoor_fw_norm: "{{ (dev2_ver ~ ' rev ' ~ dev2_rev_num) if (dev2_ver|length>0 and dev2_rev_num|length>0) else '' }}" - - - 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 - 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) - delegate_to: localhost - ansible.builtin.uri: + uri: url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" method: POST user: "{{ rmq_user }}" @@ -516,81 +229,41 @@ 'inscope_device': (ansible_hostname | default(inventory_hostname)), 'task_name': 'custom_field_set', 'task_add1': 'indoor_fwver', - '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') - ) + 'task_result': (indoor_fw_norm if indoor_fw_norm|length>0 else 'unavailable') } | to_json }}" payload_encoding: "string" - register: dev2_publish_resp changed_when: false post_tasks: - - name: "✅ Progress | Cleanup start (close tunnel & remove temp IP)" + - name: "✅ Cleanup start" debug: - msg: "Closing ControlMaster and removing temporary IP" + msg: "Closing tunnel 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 + - name: Refresh ARP #4 + raw: "{{ pathprefix }} arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3" failed_when: false ignore_errors: true - - name: Close SSH ControlMaster (best-effort) + - name: Close SSH ControlMaster delegate_to: localhost - ansible.builtin.shell: "ssh -S '{{ _ctrl_sock | default('/dev/null') }}' -O exit 2>/dev/null || true" - changed_when: false + shell: "ssh -S '{{ _ctrl_sock | default('/dev/null') }}' -O exit 2>/dev/null || true" failed_when: false ignore_errors: true - - name: Remove tunnel control dir (best-effort) + - name: Remove tunnel control dir delegate_to: localhost - ansible.builtin.file: + file: path: "{{ _ctrl_dir | default('/tmp/none') }}" state: absent ignore_errors: true - - 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 + - name: Remove temporary IP + raw: "{{ pathprefix }} ip a del {{ dev2_side_ip }} dev {{ dev1_iface }}" failed_when: false ignore_errors: true - - 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: "✅ Completed | Device processed (Dev1+Dev2)." + - name: "✅ Completed | Device processed" debug: msg: - "Device: {{ inventory_hostname }}" - - "Status: DONE (report-only)" + - "Status: DONE"