--- - name: After upgrade verification for indoor DEV2 via DEV1 tunnel hosts: all gather_facts: no vars: # ---------------- BusyBox-safe path prefix ---------------- pathprefix: "PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; " # ---------------- DEV1 (outer device) ---------------- dev1_user: "root" dev1_pass: "wavewave" dev1_iface: "br-wan" # ---------------- DEV2 (indoor behind DEV1) ---------------- dev2_host: "192.168.1.1" dev2_port: 22 dev2_side_ip: "192.168.1.11/24" dev2_ssh_user: "root" dev2_passfiles: - "basicpass" - "basicpass2" # ---------------- RabbitMQ (same env scheme as main playbooks) ---------------- 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) }}" debug_aic: false tasks: # ---- Normalize metadata from delayed message ---- - name: Normalize after upgrade metadata into facts delegate_to: localhost ansible.builtin.set_fact: attempt: "{{ (attempt | default(1)) | int }}" effective_max_attempts: "{{ (max_attempts | default(3)) | int }}" correlation_id: "{{ correlation_id | default('') }}" original_emitted_at: "{{ original_emitted_at | default('') }}" # keep whatever scheduler sent in core and full; prefer full when available target_version: "{{ target_version | default('') }}" target_version_full: "{{ target_version_full | default(target_version | default('')) }}" - name: Debug show received scheduler metadata delegate_to: localhost ansible.builtin.debug: msg: - "attempt={{ attempt }}" - "max_attempts={{ effective_max_attempts }}" - "correlation_id={{ correlation_id }}" - "original_emitted_at={{ original_emitted_at }}" - "target_version_full={{ target_version_full }}" - "target_version={{ target_version | default('') }}" # Visibility of what scheduler sent - name: Debug show received scheduler metadata delegate_to: localhost ansible.builtin.debug: msg: - "attempt={{ attempt }}" - "max_attempts={{ effective_max_attempts }}" - "correlation_id={{ correlation_id }}" - "original_emitted_at={{ original_emitted_at }}" - "target_version_full={{ target_version_full }}" # ---- Controller side TCP probe to DEV1 ---- - name: Probe DEV1 TCP 22 from controller delegate_to: localhost ansible.builtin.shell: | nc -z -w5 {{ ansible_host | default(inventory_hostname) }} 22 register: nc_probe changed_when: false ignore_errors: true # ---- If TCP down publish journal and schedule next try or give up ---- - name: Build journal payload for DEV1 TCP unreachable and mark retry when: nc_probe.rc != 0 delegate_to: localhost ansible.builtin.set_fact: fail_payload: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- afterupgrade_indoor_check (attempt {{ attempt }}/{{ effective_max_attempts }}) TCP 22 unreachable. Correlation={{ correlation_id }} Original={{ original_emitted_at }} Target='{{ target_version_full }}' _needs_retry: true - name: Publish journal for DEV1 TCP unreachable when: nc_probe.rc != 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: "{{ fail_payload | to_json }}" payload_encoding: "string" register: rmq_j_tcp_fail changed_when: (rmq_j_tcp_fail.json is defined) and (rmq_j_tcp_fail.json.routed | default(false) | bool) - name: Compute next attempt delay ten minutes and counters when: (_needs_retry | default(false)) | bool delegate_to: localhost ansible.builtin.set_fact: next_attempt: "{{ attempt | int + 1 }}" next_delay_sec: 600 next_delay_ms: 600000 - name: Build final gave up journal when max attempts reached when: (_needs_retry | default(false)) | bool and (attempt | int) >= (effective_max_attempts | int) delegate_to: localhost ansible.builtin.set_fact: journal_gaveup_payload: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- afterupgrade_indoor_check GAVE_UP (attempt {{ attempt }}/{{ effective_max_attempts }}) Exhausted attempts. Last error path=TCP. Correlation={{ correlation_id }} Original={{ original_emitted_at }} Target='{{ target_version_full }}' - name: Publish final gave up journal after TCP failures when: journal_gaveup_payload is defined 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: "{{ journal_gaveup_payload | to_json }}" payload_encoding: "string" register: rmq_j_gaveup changed_when: (rmq_j_gaveup.json is defined) and (rmq_j_gaveup.json.routed | default(false) | bool) - name: Stop host after final gave up due to TCP failure when: journal_gaveup_payload is defined ansible.builtin.meta: end_host - name: Build delayed payload for next attempt after upgrade ten minutes when: (_needs_retry | default(false)) | bool and (attempt | int) < (effective_max_attempts | int) delegate_to: localhost ansible.builtin.set_fact: delayed_payload: task_name: "afterupgrade_indoor_check" inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" attempt: "{{ next_attempt | int }}" max_attempts: "{{ effective_max_attempts | int }}" correlation_id: "{{ correlation_id }}" original_emitted_at: "{{ original_emitted_at }}" target_version_full: "{{ target_version_full }}" target_version: "{{ target_version | default(target_version_full) }}" current_delay_sec: "{{ next_delay_sec | int }}" schema_version: 1 - name: Publish delayed next attempt to holding exchange with TTL to deviceconfig when: delayed_payload is defined delegate_to: localhost ansible.builtin.uri: url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/deviceconfig.holding/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" expiration: "{{ (next_delay_ms | int) | string }}" correlation_id: "{{ correlation_id }}" routing_key: "deviceconfig" payload: "{{ delayed_payload | to_json }}" payload_encoding: "string" register: rmq_pub_next changed_when: (rmq_pub_next.json is defined) and (rmq_pub_next.json.routed | default(false) | bool) - name: Stop host after scheduling next attempt when: (_needs_retry | default(false)) | bool ansible.builtin.meta: end_host # ---------------- Step 1 Read DEV1 hostname and sanity ---------------- - name: Read DEV1 hostname from kernel procfs when: nc_probe.rc == 0 ansible.builtin.raw: > {{ pathprefix }} (cat /proc/sys/kernel/hostname 2>/dev/null || echo "") register: dev1_host_read changed_when: false - name: Stop if DEV1 hostname does not match inventory hostname ansible.builtin.meta: end_host when: (dev1_host_read.stdout | trim | length > 0) and ((dev1_host_read.stdout | trim) != (inventory_hostname | string)) # ---------------- Step 2 Setup temporary IP for reachability ---------------- - name: Add temporary IP on DEV1 ignore if address already 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(''))) # ---------------- Step 3 Start SSH tunnel via DEV1 ---------------- - name: Pick random free local port for SSH tunnel delegate_to: localhost ansible.builtin.shell: | set -e for i in $(seq 1 25); do p="$(shuf -i 20000-39999 -n1)" if ! ss -ltn | awk '{print $4}' | grep -qE "(:|\.)${p}$"; then echo "$p"; exit 0; fi done exit 1 register: pick_port changed_when: false - 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 and create control directory delegate_to: localhost ansible.builtin.set_fact: _local_port: "{{ pick_port.stdout | trim }}" _ctrl_dir: "{{ lookup('ansible.builtin.pipe', 'mktemp -d') }}" - name: Build SSH control socket path for tunnel delegate_to: localhost ansible.builtin.set_fact: _ctrl_sock: "{{ _ctrl_dir }}/ssh_tunnel_ctl" - name: Start SSH ControlMaster tunnel via DEV1 to DEV2 delegate_to: localhost ansible.builtin.shell: | set -e sshpass -p '{{ dev1_pass }}' ssh -f -N \ -M -S "{{ _ctrl_sock }}" \ -L "127.0.0.1:{{ _local_port }}:{{ dev2_host }}:{{ dev2_port }}" \ -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -o ConnectTimeout=15 \ "{{ dev1_user }}@{{ ansible_host | default(inventory_hostname) }}" args: executable: /bin/bash register: start_tunnel changed_when: true # --- settle and check ControlMaster and TCP probe --- - name: Pause briefly to let SSH tunnel settle delegate_to: localhost ansible.builtin.wait_for: timeout: 1 changed_when: false - name: Verify tunnel ControlMaster is running with ssh O check delegate_to: localhost ansible.builtin.shell: | set -e HOST="{{ ansible_host | default(inventory_hostname) }}" ssh -S "{{ _ctrl_sock }}" -O check "{{ dev1_user }}@${HOST}" 2>&1 || true register: tun_check changed_when: false - name: Debug show tunnel master check result delegate_to: localhost ansible.builtin.debug: msg: - "tunnel_check.rc={{ tun_check.rc }}" - "tunnel_check.out={{ (tun_check.stdout | default('')) | trim }}" - name: Probe TCP reachability through tunnel to DEV2 delegate_to: localhost ansible.builtin.shell: | set -e nc -z -w5 127.0.0.1 "{{ _local_port }}" register: nc_probe changed_when: false ignore_errors: true - name: Debug show reachability probe result through tunnel delegate_to: localhost ansible.builtin.debug: msg: - "nc.rc={{ nc_probe.rc }}" - "nc.stdout={{ (nc_probe.stdout | default('')) | trim }}" - "nc.stderr={{ (nc_probe.stderr | default('')) | trim }}" - name: Stop if tunnel reachability check failed ansible.builtin.meta: end_host when: nc_probe.rc != 0 # ---------------- Step 4 Determine working password for DEV2 ---------------- - name: Try provided passfiles to authenticate to DEV2 delegate_to: localhost ansible.builtin.shell: | for f in {{ dev2_passfiles | join(' ') }}; do if sshpass -f "$f" ssh -p {{ _local_port }} -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -o ConnectTimeout=10 root@127.0.0.1 "echo OK" >/dev/null 2>&1; then echo "$f"; exit 0; fi done echo NONE register: dev2_passfile_try changed_when: false - name: Save selected DEV2 passfile for subsequent commands delegate_to: localhost ansible.builtin.set_fact: dev2_passfile_used: "{{ (dev2_passfile_try.stdout | trim) }}" changed_when: false - name: Debug show which DEV2 passfile was selected delegate_to: localhost ansible.builtin.debug: msg: "dev2_passfile_used={{ dev2_passfile_used }}" - name: Stop if no valid DEV2 passfile was found ansible.builtin.meta: end_host when: dev2_passfile_used == "NONE" # ---------------- Step 5 Read firmware version on DEV2 ---------------- - name: Read firmware version or banner from DEV2 delegate_to: localhost ansible.builtin.shell: | sshpass -f "{{ dev2_passfile_used }}" ssh -p {{ _local_port }} \ -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -o ConnectTimeout=10 \ root@127.0.0.1 "cat /usr/lib/release/firmux 2>/dev/null || grep -i rev /etc/banner 2>/dev/null || echo unknown" register: dev2_fwver changed_when: false ignore_errors: true - name: Debug | banner normalization inputs delegate_to: localhost when: (debug_aic | default(false)) | bool ansible.builtin.debug: msg: - "banner_raw={{ (dev2_fwver.stdout | default('') | trim) }}" - "normalize rule: '2.2.1 rev 6801' -> '2.2.1-r6801'" - name: Debug show raw firmware readout from DEV2 delegate_to: localhost ansible.builtin.debug: msg: "Firmware version on DEV2 {{ (dev2_fwver.stdout | default('')) | trim }}" # ---------------- Retry metadata and success evaluation ---------------- - name: Normalize retry control facts for indoor checker ansible.builtin.set_fact: attempt: "{{ (attempt | default(1)) | int }}" effective_max_attempts: "{{ (max_attempts | default(3)) | int }}" correlation_id: "{{ correlation_id | default('') }}" original_emitted_at: "{{ original_emitted_at | default('') }}" - name: Compute read_ok flag based on firmware readout delegate_to: localhost ansible.builtin.set_fact: read_ok: "{{ (dev2_fwver.rc | default(1) == 0) and ((dev2_fwver.stdout | default('') | trim) | length > 0) and (not (((dev2_fwver.stdout | default('unknown')) | lower) is search('unknown'))) }}" # ===================== Normalization and comparison ===================== # Compute expected_norm from target_version_full. If a whole filename is sent, # extract X.Y.Z-rNNNN otherwise keep the original trimmed string. - name: Normalize expected target step one compute base string delegate_to: localhost ansible.builtin.set_fact: expected_norm_step1: "{{ (target_version_full | default('') | trim) }}" - name: Debug | expected target base and regex extraction inputs delegate_to: localhost when: debug_aic | bool ansible.builtin.debug: msg: - "target_version(raw)={{ target_version | default('') }}" - "target_version_full(raw)={{ target_version_full | default('') }}" - "regex='[0-9]+\\.[0-9]+\\.[0-9]+-r[0-9]+'" - name: Extract version core X dot Y dot Z dash rNNNN from target if present delegate_to: localhost ansible.builtin.set_fact: expected_norm_core_list: "{{ (target_version_full | default('') | regex_findall('[0-9]+\\.[0-9]+\\.[0-9]+-r[0-9]+')) | default([]) }}" - name: Choose first extracted core if available delegate_to: localhost ansible.builtin.set_fact: expected_norm_core: "{{ (expected_norm_core_list | default([]) | length > 0) | ternary((expected_norm_core_list | first), '') }}" - name: Normalize expected target step two prefer extracted core when available delegate_to: localhost ansible.builtin.set_fact: expected_norm: "{{ (expected_norm_core | default('') | length > 0) | ternary(expected_norm_core, expected_norm_step1) }}" - name: Debug | final expected normalized version (what we will compare against) delegate_to: localhost when: debug_aic | bool ansible.builtin.debug: msg: - "expected_norm={{ expected_norm | default('') }}" # 1 capture raw banner line from DEV2 - name: Capture firmware banner line from DEV2 delegate_to: localhost ansible.builtin.set_fact: banner_raw: "{{ (dev2_fwver.stdout | default('') | trim) }}" # 2 normalize rev suffix to dash rNNNN - name: Normalize banner line to X dot Y dot Z dash rNNNN suffix delegate_to: localhost ansible.builtin.set_fact: banner_norm: >- {{ ((banner_raw | lower) is search('-r[0-9]+$')) | ternary( banner_raw, (banner_raw | regex_replace('\s*[Rr][Ee][Vv]\.?\s*([0-9]+)\s*$', '-r\1')) ) }} - name: Debug | banner normalized result delegate_to: localhost when: debug_aic | bool ansible.builtin.debug: msg: - "banner_norm={{ banner_norm | default('') }}" - name: Debug | version compare inputs and strategy delegate_to: localhost when: debug_aic | bool ansible.builtin.debug: msg: - "expected_norm={{ expected_norm | default('') }}" - "banner_norm={{ banner_norm | default('') }}" - "eq={{ (expected_norm | default('')) == (banner_norm | default('')) }}" - "substr={{ (banner_norm | default('')) is search((expected_norm | default('')), ignorecase=False) if (expected_norm | default('') | length) > 0 else 'N/A' }}" - name: Evaluate version match using normalized equality or substring delegate_to: localhost ansible.builtin.set_fact: version_match: >- {{ (expected_norm | length > 0) and ( (banner_norm == expected_norm) or ((banner_norm | lower) is search((expected_norm | lower))) or ((expected_norm | lower) is search((banner_norm | lower))) ) }} - name: Debug | version match outcome delegate_to: localhost when: debug_aic | bool ansible.builtin.debug: msg: - "version_match={{ version_match | default(false) }}" - name: Debug snapshot of expected and actual normalized versions delegate_to: localhost ansible.builtin.debug: msg: - "expected_norm={{ expected_norm }}" - "banner_norm={{ banner_norm }}" - "version_match={{ version_match | default(false) }}" # ===================== Journaling and tagging paths ===================== # SUCCESS path - name: Build journal payload for successful after upgrade check when: (read_ok | bool) and (version_match | bool) delegate_to: localhost ansible.builtin.set_fact: journal_success_payload: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- afterupgrade_indoor_check SUCCESS (attempt {{ attempt }}/{{ effective_max_attempts }}) Banner='{{ banner_raw }}' Target='{{ expected_norm }}' Correlation={{ correlation_id | default('') }} Original={{ original_emitted_at | default('') }} - name: Publish success journal to control queue when: journal_success_payload is defined 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: "{{ journal_success_payload | to_json }}" payload_encoding: "string" register: rmq_pub_success changed_when: (rmq_pub_success.json is defined) and (rmq_pub_success.json.routed | default(false) | bool) - name: Build payload to add tag indoor update success on DEV1 when: journal_success_payload is defined delegate_to: localhost ansible.builtin.set_fact: tag_add_payload: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "tag_add" task_result: "indoor-update-success" - name: Publish tag indoor update success when: tag_add_payload is defined 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: "{{ tag_add_payload | to_json }}" payload_encoding: "string" register: rmq_tag_add_success changed_when: (rmq_tag_add_success.json is defined) and (rmq_tag_add_success.json.routed | default(false) | bool) - name: Build payload to remove tag indoor restart scheduled on DEV1 when: journal_success_payload is defined delegate_to: localhost ansible.builtin.set_fact: tag_remove_payload: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "tag_remove" task_result: "indoor-restart-scheduled" - name: Publish tag removal indoor restart scheduled when: tag_remove_payload is defined 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: "{{ tag_remove_payload | to_json }}" payload_encoding: "string" register: rmq_tag_remove_sched changed_when: (rmq_tag_remove_sched.json is defined) and (rmq_tag_remove_sched.json.routed | default(false) | bool) # --- Normalize firmware string and set custom field on success --- - name: Capture raw firmware banner for normalization on success path when: journal_success_payload is defined delegate_to: localhost ansible.builtin.set_fact: fw_banner_raw: "{{ banner_raw }}" - name: Normalize firmware string for indoor_fwver to X dot Y dot Z dash rNNNN when: - journal_success_payload is defined - (fw_banner_raw | default('') | length) > 0 delegate_to: localhost ansible.builtin.set_fact: fw_norm: >- {{ (fw_banner_raw | lower) is search('-r[0-9]+$') | ternary( fw_banner_raw, (fw_banner_raw | regex_replace('\\s*[Rr][Ee][Vv]\\.?\\s*([0-9]+)\\s*$', '-r\\1')) ) }} - name: Debug show normalized indoor firmware value when: fw_norm is defined delegate_to: localhost ansible.builtin.debug: msg: "Normalized indoor_fwver={{ fw_norm }} (from {{ fw_banner_raw }})" - name: Publish custom field indoor_fwver to control queue when: - fw_norm is defined - (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': fw_norm } | to_json }}" payload_encoding: "string" register: rmq_customfield_fw changed_when: (rmq_customfield_fw.json is defined) and (rmq_customfield_fw.json.routed | default(false) | bool) # MISMATCH path - name: Build journal payload for version mismatch after upgrade when: (read_ok | bool) and (not (version_match | bool)) delegate_to: localhost ansible.builtin.set_fact: journal_mismatch_payload: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- afterupgrade_indoor_check MISMATCH (attempt {{ attempt }}/{{ effective_max_attempts }}) Expected='{{ expected_norm }}' Got='{{ banner_raw }}' Correlation={{ correlation_id | default('') }} Original={{ original_emitted_at | default('') }} - name: Publish journal for version mismatch when: journal_mismatch_payload is defined 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: "{{ journal_mismatch_payload | to_json }}" payload_encoding: "string" register: rmq_j_mismatch changed_when: (rmq_j_mismatch.json is defined) and (rmq_j_mismatch.json.routed | default(false) | bool) - name: Stop host after mismatch path is handled when: journal_mismatch_payload is defined ansible.builtin.meta: end_host # FAILURE and RETRY path - name: Build journal payload for indoor firmware read failure when: not (read_ok | bool) delegate_to: localhost ansible.builtin.set_fact: journal_fail_payload: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- afterupgrade_indoor_check FAILED (attempt {{ attempt }}/{{ effective_max_attempts }}) fwread_rc={{ dev2_fwver.rc | default('NA') }}, output='{{ (dev2_fwver.stdout | default('') | trim) }}' Correlation={{ correlation_id | default('') }} Original={{ original_emitted_at | default('') }} - name: Publish journal for indoor firmware read failure when: journal_fail_payload is defined 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: "{{ journal_fail_payload | to_json }}" payload_encoding: "string" register: rmq_pub_fail changed_when: (rmq_pub_fail.json is defined) and (rmq_pub_fail.json.routed | default(false) | bool) - name: Compute retry parameters wait ten minutes before next attempt when: not (read_ok | bool) ansible.builtin.set_fact: next_attempt: "{{ (attempt | int) + 1 }}" next_delay_sec: 600 next_delay_ms: "{{ 600000 }}" - name: Build final gave up journal when max attempts reached after read failure when: (not (read_ok | bool)) and ((attempt | int) >= (effective_max_attempts | int)) delegate_to: localhost ansible.builtin.set_fact: journal_gaveup_payload: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- afterupgrade_indoor_check GAVE_UP (attempt {{ attempt }}/{{ effective_max_attempts }}) Exhausted attempts. Last fwread_rc={{ dev2_fwver.rc | default('NA') }}. Correlation={{ correlation_id | default('') }} Original={{ original_emitted_at | default('') }} - name: Publish final gave up journal after read failure when: journal_gaveup_payload is defined 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: "{{ journal_gaveup_payload | to_json }}" payload_encoding: "string" register: rmq_pub_gaveup changed_when: (rmq_pub_gaveup.json is defined) and (rmq_pub_gaveup.json.routed | default(false) | bool) # Only schedule next attempt if budget left - name: Build delayed payload for next indoor attempt wait ten minutes when: (not (read_ok | bool)) and ((attempt | int) < (effective_max_attempts | int)) delegate_to: localhost ansible.builtin.set_fact: delayed_payload: task_name: "afterupgrade_indoor_check" inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" attempt: "{{ next_attempt | int }}" max_attempts: "{{ effective_max_attempts | int }}" correlation_id: "{{ correlation_id | default('') }}" original_emitted_at: "{{ original_emitted_at | default('') }}" target_version_full: "{{ target_version_full }}" target_version: "{{ target_version | default(target_version_full) }}" current_delay_sec: "{{ next_delay_sec | int }}" schema_version: 1 - name: Debug | next attempt backoff plan delegate_to: localhost when: debug_aic | bool and (version_match | default(false) | bool) == false ansible.builtin.debug: msg: - "attempt={{ attempt | default('') }} / max_attempts={{ max_attempts | default('') }}" - "next_delay_sec={{ next_delay_sec | default('') }}" - "routing_key={{ routing_key | default('deviceconfig') }}" - "correlation_id={{ correlation_id | default('') }}" - name: Publish delayed next indoor attempt to holding with TTL routed to deviceconfig when: delayed_payload is defined delegate_to: localhost ansible.builtin.uri: url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/deviceconfig.holding/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" expiration: "{{ (next_delay_ms | int) | string }}" correlation_id: "{{ correlation_id | default('') }}" routing_key: "deviceconfig" payload: "{{ delayed_payload | to_json }}" payload_encoding: "string" register: rmq_pub_next changed_when: (rmq_pub_next.json is defined) and (rmq_pub_next.json.routed | default(false) | bool) post_tasks: - name: Cleanup section tunnel and temporary IP block: - ansible.builtin.debug: msg: "Cleaning up tunnel and temporary IP" changed_when: false delegate_to: localhost always: - name: Close SSH ControlMaster tunnel 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 temporary control directory best effort delegate_to: localhost ansible.builtin.file: path: "{{ _ctrl_dir | default('/tmp/none') }}" state: absent ignore_errors: true - name: Remove temporary IP from DEV1 best effort ansible.builtin.raw: > {{ pathprefix }} ip a del {{ dev2_side_ip }} dev {{ dev1_iface }} register: del_ip failed_when: false changed_when: false