diff --git a/files/ansible-playbooks/update-outdoorbackup-scheduler.yml b/files/ansible-playbooks/update-outdoorbackup-scheduler.yml new file mode 100644 index 0000000..eab6a7b --- /dev/null +++ b/files/ansible-playbooks/update-outdoorbackup-scheduler.yml @@ -0,0 +1,97 @@ +# update-outdoorbackup-scheduler.yml +# Single nbplay invocation: +# Phase 1: Cloud -> NetBox sync (via sot-updater-iponly.yml in wrapper mode) +# Phase 2: Refresh ansible_host from nb_ip, reset_connection +# Phase 3: Run update-outdoorbackup224.yml + + +- hosts: all + gather_facts: no + vars: + 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: [] + +- import_playbook: sot-updater-iponly.yml + vars: + sot_wrapper_mode: true + + +- name: Phase 2 | Refresh in-memory target IP after iponly + hosts: all + gather_facts: no + vars: + 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: + - name: "Wrapper | Extract nb_ip from nb_onedevice_update.py stdout" + vars: + _lines: "{{ (nb_preflight.stdout_lines | default([])) | map('regex_replace','\r','') | map('trim') | list }}" + _ok_line: "{{ (_lines | select('match','^OK\\s+') | list | last | default('')) }}" + _fail_line: "{{ (_lines | select('match','^FAIL\\s+') | list | last | default('')) }}" + _ip_raw: "{{ _ok_line | regex_search('([0-9]{1,3}(?:\\.[0-9]{1,3}){3})') | default('') }}" + set_fact: + nb_ip: "{{ _ip_raw }}" + nb_ip_ok: "{{ (_ip_raw | length) > 0 }}" + nb_ip_err: "{{ _fail_line }}" + changed_when: false + + - name: "Wrapper | Abort if nb_ip not available" + fail: + msg: "Wrapper preflight did not produce nb_ip. FAIL line: {{ nb_ip_err | default('') }}" + when: not nb_ip_ok + + - name: "Wrapper | Debug before ansible_host override" + debug: + msg: + - "inventory_hostname={{ inventory_hostname }}" + - "ansible_host(before)={{ ansible_host | default('UNSET') }}" + - "ansible_port={{ ansible_port | default('22') }}" + - "nb_ip={{ nb_ip | default('UNSET') }}" + changed_when: false + + - name: "Wrapper | Update ansible_host to nb_ip" + set_fact: + ansible_host: "{{ nb_ip }}" + changed_when: false + + - name: "Wrapper | Debug after ansible_host override" + debug: + msg: + - "inventory_hostname={{ inventory_hostname }}" + - "ansible_host(after)={{ ansible_host | default('UNSET') }}" + - "ansible_port={{ ansible_port | default('22') }}" + changed_when: false + + - name: "Wrapper | Reset connection after ansible_host change" + meta: reset_connection + + - name: "Wrapper | Debug after reset_connection" + debug: + msg: + - "inventory_hostname={{ inventory_hostname }}" + - "ansible_host(post_reset)={{ ansible_host | default('UNSET') }}" + - "ansible_port={{ ansible_port | default('22') }}" + changed_when: false + + + - name: "Wrapper | Pause 2s before next playbook" + pause: + seconds: 2 + changed_when: false + +- import_playbook: update-outdoorbackup224.yml + vars: + is_run_by: "scheduler" diff --git a/files/ansible-playbooks/update-outdoorbackup224.yml b/files/ansible-playbooks/update-outdoorbackup224.yml new file mode 100644 index 0000000..27c4c55 --- /dev/null +++ b/files/ansible-playbooks/update-outdoorbackup224.yml @@ -0,0 +1,848 @@ +--- +- name: Upgrade firmware safely (no Python on target) + hosts: all + gather_facts: no + + vars: + # RabbitMQ (pull from env if provided) + 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) }}" + + # Invocation context (default: manual; scheduler wrapper sets is_run_by=scheduler) + is_run_by_effective: "{{ is_run_by | default('manual') }}" + + # NEW: Post-upgrade check scheduling (via holding queue -> DLX) + # The holding queue is bound to exchange 'deviceconfig.holding' with routing key 'deviceconfig'. + # Messages published here carry a per-message TTL (AMQP 'expiration' property, in ms). + # Once TTL elapses, messages dead-letter to exchange 'deviceconfig' with same routing key, + # where a consumer will perform the after-upgrade verification (attempt-based backoff lives on consumer side). + afterupgrade_hold_exchange: "{{ lookup('env','AFTERUP_HOLD_EXCHANGE') | default('deviceconfig.holding', true) }}" + afterupgrade_routing_key: "{{ lookup('env','AFTERUP_ROUTING_KEY') | default('deviceconfig', true) }}" + # Queue name is not used for publish; present for documentation/reference only + afterupgrade_hold_queue: "{{ lookup('env','AFTERUP_HOLD_QUEUE') | default('queue_deviceconfig_holdingzone', true) }}" + + # REQUIRED (pass via -e) + firmware_path: /tmp/2.2.4-r9850.bin + firmware_sha256: "38f7dd3bb5b06a2267d7bc68e2d8351df59c2aea858d644909208e11a3970539" + + # Helper computed vars + fw_base: "{{ firmware_path | basename }}" + fw_name: "{{ fw_base | regex_replace('\\.bin$', '') }}" + fw_banner_repr: "{{ fw_name | regex_replace('-r', ' rev ') }}" + fw_marker: "/tmp/prepared_for_{{ fw_name }}" + pathprefix: "PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; " + + tasks: + + # ----------------------------- HOSTNAME PREFLIGHT ----------------------------- + - name: Hostname preflight + block: + - name: Read remote HOSTNAME + ansible.builtin.raw: "{{ pathprefix }} echo \"$HOSTNAME\"" + register: host_env + changed_when: false + + - name: Debug hostnames + ansible.builtin.debug: + msg: + - "remote_hostname={{ host_env.stdout | trim }}" + - "inventory_hostname={{ inventory_hostname }}" + + - name: Stop if connected hostname differs from inventory + ansible.builtin.fail: + msg: "Aborting: connected host reported hostname '{{ host_env.stdout | trim }}' which differs from inventory '{{ inventory_hostname }}'." + when: (host_env.stdout | trim) != inventory_hostname + + rescue: + - name: Build failure task name and detail (hostname preflight) + ansible.builtin.set_fact: + fail_task_name: "{{ ansible_failed_task.name | default('hostname preflight') }}" + fail_detail_raw: >- + {{ ansible_failed_result.msg + | default(ansible_failed_result.stderr) + | default(ansible_failed_result.stdout) + | default('no additional error output') + | trim }} + + - name: Build failure summary text (hostname preflight) + ansible.builtin.set_fact: + fail_summary: >- + Firmware update aborted at '{{ fail_task_name }}': {{ fail_detail_raw }} + + - name: Truncate failure summary to ~400 chars (hostname preflight) + ansible.builtin.set_fact: + fail_summary_short: "{{ fail_summary | regex_replace('\\s+', ' ') | trim | truncate(400, True, '...') }}" + + - name: Build control queue payload for failure journal (hostname preflight) + ansible.builtin.set_fact: + journal_failure_payload_pre: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "journal_add" + task_result: "{{ fail_summary_short }}" + + - name: Bandaid | Pause before Publish failure journal to control queue (hostname preflight) + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish failure journal to control queue (hostname preflight) + 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_failure_payload_pre | to_json }}" + payload_encoding: "string" + register: rmq_journal_pre_resp + changed_when: (rmq_journal_pre_resp.json is defined) and (rmq_journal_pre_resp.json.routed | default(false) | bool) + failed_when: > + (rmq_journal_pre_resp.status != 200) or + (rmq_journal_pre_resp.json is not defined) or + (not (rmq_journal_pre_resp.json.routed | default(false) | bool)) + delegate_to: localhost + + - name: Build control queue payload for update-aborted tag (hostname preflight) + ansible.builtin.set_fact: + tag_failed_payload_pre: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "tag_add" + task_result: "update-aborted" + + - name: Bandaid | Pause before Publish update-aborted tag to control queue (hostname preflight) + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish update-aborted tag to control queue (hostname preflight) + 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_failed_payload_pre | to_json }}" + payload_encoding: "string" + register: rmq_tag_failed_pre_resp + changed_when: (rmq_tag_failed_pre_resp.json is defined) and (rmq_tag_failed_pre_resp.json.routed | default(false) | bool) + failed_when: > + (rmq_tag_failed_pre_resp.status != 200) or + (rmq_tag_failed_pre_resp.json is not defined) or + (not (rmq_tag_failed_pre_resp.json.routed | default(false) | bool)) + delegate_to: localhost + + - name: Scheduler | Publish action_state failed (hostname preflight) + 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': 'action_state', + 'task_result': 'failed' + } | to_json }}" + payload_encoding: "string" + changed_when: false + delegate_to: localhost + when: is_run_by_effective == 'scheduler' + + - name: Scheduler | Debug action_state failed published (hostname preflight) + ansible.builtin.debug: + msg: "scheduler-run detected; published action_state=failed (hostname preflight)" + when: is_run_by_effective == 'scheduler' + + - name: Stop play after hostname preflight failure + ansible.builtin.meta: end_play + + - name: Scheduler context | Debug is_run_by + ansible.builtin.debug: + msg: "is_run_by={{ is_run_by_effective }}" + + - name: Scheduler | Publish action_state inprogress (scheduler-run) + 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': 'action_state', + 'task_result': 'inprogress' + } | to_json }}" + payload_encoding: "string" + changed_when: false + delegate_to: localhost + when: is_run_by_effective == 'scheduler' + + - name: Scheduler | Debug action_state inprogress published + ansible.builtin.debug: + msg: "scheduler-run detected; published action_state=inprogress" + when: is_run_by_effective == 'scheduler' + + # --- Tag device as update-in-progress at start --- + - name: Build control queue payload for update-in-progress tag + ansible.builtin.set_fact: + tag_inprogress_payload: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "tag_add" + task_result: "update-in-progress" + + - name: Bandaid | Pause before Publish update-in-progress tag to control queue via RabbitMQ HTTP API + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish update-in-progress tag to control queue via RabbitMQ HTTP API + 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_inprogress_payload | to_json }}" + payload_encoding: "string" + register: rmq_tag_inprogress_resp + changed_when: (rmq_tag_inprogress_resp.json is defined) and (rmq_tag_inprogress_resp.json.routed | default(false) | bool) + failed_when: > + (rmq_tag_inprogress_resp.status != 200) or + (rmq_tag_inprogress_resp.json is not defined) or + (not (rmq_tag_inprogress_resp.json.routed | default(false) | bool)) + delegate_to: localhost + + - name: Log control queue tag publish result + ansible.builtin.debug: + var: rmq_tag_inprogress_resp.json + when: rmq_tag_inprogress_resp is defined + + # --------------------- Prepared marker check BEFORE SSID scan ----------------- + - name: Check if any prepared marker exists + ansible.builtin.raw: "{{ pathprefix }} [ -e /tmp/prepared_for* ] && echo PRESENT || echo ABSENT" + register: prep_scan + changed_when: false + + - name: Debug marker presence + ansible.builtin.debug: + msg: "prepared_marker={{ prep_scan.stdout | trim }}" + + - name: Journal preparation already present, skipping update steps + 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_add", + "task_result": ( + "Preparation already present for " ~ fw_banner_repr ~ + "; marker " ~ fw_marker ~ + ". Skipping update steps." + ) + } | to_json + }} + payload_encoding: "string" + register: rmq_journal_prep_present + changed_when: (rmq_journal_prep_present.json is defined) and (rmq_journal_prep_present.json.routed | default(false) | bool) + failed_when: > + (rmq_journal_prep_present.status != 200) or + (rmq_journal_prep_present.json is not defined) or + (not (rmq_journal_prep_present.json.routed | default(false) | bool)) + delegate_to: localhost + when: (prep_scan.stdout | trim) == 'PRESENT' + + + - name: End play for this host (already prepared) + ansible.builtin.meta: end_host + when: (prep_scan.stdout | trim) == 'PRESENT' + + # --- SSID scan & journal (does not stop the play) --- + - name: Count SSID lines in /tmp/config.json (filtered) + ansible.builtin.raw: > + {{ pathprefix }} + grep '"ssid"' /tmp/config.json 2>/dev/null | grep -vE '\{|SC|auto|backha' | wc -l + register: ssid_lines + changed_when: false + + - name: Debug SSID count + + ansible.builtin.debug: + msg: "ssid_count={{ (ssid_lines.stdout | default('0')) | trim }}" + + - name: Build joined SSID list when multiple SSIDs found (≥3) + ansible.builtin.raw: > + {{ pathprefix }} + grep '"ssid"' /tmp/config.json | grep -vE '\{|SC|auto|backha' \ + | sed -E 's/.*"ssid": "([^"]+)".*/\1/' \ + | awk 'NR==1 { out=$0; next } { out=out","$0 } END { print out }' + register: ssid_concat + changed_when: false + when: (ssid_lines.stdout | trim | int) >= 3 + + - name: Build control queue payload for SSID journal (journal_add) + ansible.builtin.set_fact: + ssid_journal_payload: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "journal_add" + task_result: "Multiple SSID! {{ ssid_concat.stdout | trim }}" + when: (ssid_lines.stdout | trim | int) >= 3 + + - name: Bandaid | Pause before Publish SSID journal to control queue via RabbitMQ HTTP API + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish SSID journal to control queue via RabbitMQ HTTP API + 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: "{{ ssid_journal_payload | to_json }}" + payload_encoding: "string" + register: rmq_ssid_journal_resp + changed_when: (rmq_ssid_journal_resp.json is defined) and (rmq_ssid_journal_resp.json.routed | default(false) | bool) + failed_when: > + (rmq_ssid_journal_resp.status != 200) or + (rmq_ssid_journal_resp.json is not defined) or + (not (rmq_ssid_journal_resp.json.routed | default(false) | bool)) + delegate_to: localhost + when: ssid_journal_payload is defined + + # ----------------------------- MAIN UPDATE BLOCK ----------------------------- + - name: Firmware update main block + block: + + - name: Check if firmware image is already on the device + ansible.builtin.raw: "{{ pathprefix }} [ -f '{{ firmware_path }}' ] && echo OK || echo MISSING" + register: fw_exists + changed_when: false + + - name: Count fw_printenv lines + ansible.builtin.raw: "{{ pathprefix }} fw_printenv 2>/dev/null | wc -l" + register: env_line_count + changed_when: false + + - name: Debug fw_printenv line count + ansible.builtin.debug: + msg: "fw_printenv_lines={{ env_line_count.stdout | trim }}" + + - name: Stop if bootloader environment looks invalid (<7 lines) + ansible.builtin.fail: + msg: "Aborting: fw_printenv returned only {{ env_line_count.stdout | trim }} lines (<7) — environment missing or corrupted." + when: (env_line_count.stdout | trim | int) < 7 + + - name: Read first line of /etc/banner (current running version) + ansible.builtin.raw: "{{ pathprefix }} cat /etc/banner | grep -i rev | head -n1" + register: banner + changed_when: false + + - name: current version + ansible.builtin.debug: + msg: + - "current banner: {{ banner.stdout | trim }}" + + - name: Stop if target version matches current (/etc/banner already at {{ fw_banner_repr }}) + ansible.builtin.fail: + msg: "Aborting: device already runs {{ fw_banner_repr }} (banner: {{ banner.stdout | trim }})" + when: banner.stdout is search(fw_banner_repr) + + - name: Upload firmware to /tmp via scp (controller-side) + ansible.builtin.shell: | + set -e + SRC='{{ fw_src_local | default("/opt/containers/ansible-worker/app/2.2.4-r9850.bin") }}' + DST_USER='{{ ansible_user | default("root") }}' + DST_HOST='{{ ansible_host | default(inventory_hostname) }}' + test -f "$SRC" + sshpass -p '{{ ansible_ssh_pass }}' scp -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + "$SRC" "${DST_USER}@${DST_HOST}:{{ firmware_path }}" + delegate_to: localhost + when: fw_exists.stdout is not defined or (fw_exists.stdout | trim) != 'OK' + changed_when: true + + - name: Re-check firmware presence after optional upload + ansible.builtin.raw: "{{ pathprefix }} test -f '{{ firmware_path }}' && echo OK || echo MISSING" + register: fw_exists2 + changed_when: false + failed_when: (fw_exists2.stdout | trim) != 'OK' + + - name: Compute sha256 of the uploaded image + ansible.builtin.raw: "{{ pathprefix }} sha256sum '{{ firmware_path }}' | awk '{print $1}'" + register: sha_out + changed_when: false + + - name: Verify sha256 matches expected + ansible.builtin.fail: + msg: "SHA256 mismatch for {{ firmware_path }}. Got {{ sha_out.stdout | trim }}, expected {{ firmware_sha256 }}" + when: (sha_out.stdout | trim) != (firmware_sha256 | trim) + + - name: sha256 verification debug + ansible.builtin.debug: + msg: + - "sha256sum is: {{ sha_out.stdout | trim }}" + + - name: Check image validity (update -c must say 'valid') + ansible.builtin.raw: "{{ pathprefix }} update -c '{{ firmware_path }}'" + register: up_check + changed_when: false + failed_when: up_check.stdout.strip() != 'valid' + + - name: image verification debug + ansible.builtin.debug: + msg: + - ".bin verification is: {{ up_check.stdout | trim }}" + + # - name: forced stop before writing + # ansible.builtin.meta: end_play + + - name: Write image (this will take a while) + ansible.builtin.raw: "{{ pathprefix }} update -w '{{ firmware_path }}'" + register: up_write + changed_when: true + failed_when: up_write.stdout is not search('update is complete') + - name: Create prepared marker + ansible.builtin.raw: "{{ pathprefix }} touch '{{ fw_marker }}'" + changed_when: true + + - name: Build control queue payload (progress & target version) + ansible.builtin.set_fact: + nbq2_payload_obj: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "update_outdoorbackup" + task_result: "backup_prepared" + task_add1: "{{ fw_banner_repr }}" # e.g., "2.2.0 rev 9739" + when: up_write is changed + + - name: Bandaid | Pause before Publish message to control queue via RabbitMQ HTTP API + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish message to control queue via RabbitMQ HTTP API + 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: "{{ nbq2_payload_obj | to_json }}" + payload_encoding: "string" + register: rmq_resp + changed_when: (rmq_resp.json is defined) and (rmq_resp.json.routed | default(false)) + failed_when: > + (rmq_resp.status != 200) or + (rmq_resp.json is not defined) or + (rmq_resp.json.routed | default(false) | bool == false) + delegate_to: localhost + when: nbq2_payload_obj is defined + + - name: Log control queue publish result + ansible.builtin.debug: + var: rmq_resp.json + when: rmq_resp is defined + + - name: Summary + ansible.builtin.debug: + msg: + - "Banner before: {{ banner.stdout | trim }}" + - "Target version: {{ fw_banner_repr }}" + - "SHA256: OK ({{ sha_out.stdout | trim }})" + - "update -c: {{ up_check.stdout | trim }}" + - "update -w: OK" + - "Marker: {{ fw_marker }}" + + # --- Success tag selection (ONLY CHANGE) --- (ONLY CHANGE) --- + - name: Build control queue payload for update-backup-prepared (rebootin == 0) + ansible.builtin.set_fact: + tag_auto_restarted_payload: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "tag_add" + task_result: "update-backup-prepared" + when: + - nbq2_payload_obj is defined + - rebootin is defined + - (rebootin | int) == 0 + + - name: Build control queue payload for update-backup-prepared (rebootin >= 1) + ansible.builtin.set_fact: + tag_restart_scheduled_payload: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "tag_add" + task_result: "update-backup-prepared" + when: + - nbq2_payload_obj is defined + - rebootin is defined + - (rebootin | int) >= 1 + + - name: Build control queue payload for update-backup-prepared tag (no reboot scheduled) + ansible.builtin.set_fact: + tag_waits_restart_payload: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "tag_add" + task_result: "update-backup-prepared" + when: + - nbq2_payload_obj is defined + - rebootin is not defined + + # --- Publish chosen tag (updated names only) --- + - name: Bandaid | Pause before Publish update-backup-prepared tag to control queue via RabbitMQ HTTP API + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish update-backup-prepared tag to control queue via RabbitMQ HTTP API + 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_waits_restart_payload | to_json }}" + payload_encoding: "string" + register: rmq_tag_waits_restart_resp + changed_when: (rmq_tag_waits_restart_resp.json is defined) and (rmq_tag_waits_restart_resp.json.routed | default(false) | bool) + failed_when: > + (rmq_tag_waits_restart_resp.status != 200) or + (rmq_tag_waits_restart_resp.json is not defined) or + (not (rmq_tag_waits_restart_resp.json.routed | default(false) | bool)) + delegate_to: localhost + when: tag_waits_restart_payload is defined + + - name: Bandaid | Pause before Publish update-backup-prepared tag to control queue via RabbitMQ HTTP API + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish update-backup-prepared tag to control queue via RabbitMQ HTTP API + 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_auto_restarted_payload | to_json }}" + payload_encoding: "string" + register: rmq_tag_auto_restarted_resp + changed_when: (rmq_tag_auto_restarted_resp.json is defined) and (rmq_tag_auto_restarted_resp.json.routed | default(false) | bool) + failed_when: > + (rmq_tag_auto_restarted_resp.status != 200) or + (rmq_tag_auto_restarted_resp.json is not defined) or + (not (rmq_tag_auto_restarted_resp.json.routed | default(false) | bool)) + delegate_to: localhost + when: tag_auto_restarted_payload is defined + + - name: Bandaid | Pause before Publish update-backup-prepared tag to control queue via RabbitMQ HTTP API + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish update-backup-prepared tag to control queue via RabbitMQ HTTP API + 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_restart_scheduled_payload | to_json }}" + payload_encoding: "string" + register: rmq_tag_restart_scheduled_resp + changed_when: (rmq_tag_restart_scheduled_resp.json is defined) and (rmq_tag_restart_scheduled_resp.json.routed | default(false) | bool) + failed_when: > + (rmq_tag_restart_scheduled_resp.status != 200) or + (rmq_tag_restart_scheduled_resp.json is not defined) or + (not (rmq_tag_restart_scheduled_resp.json.routed | default(false) | bool)) + delegate_to: localhost + when: tag_restart_scheduled_payload is defined + + # --- Journal: preparation successful (only if fully successful) --- + - name: Build control queue payload for success journal + ansible.builtin.set_fact: + journal_success_payload: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "journal_add" + task_result: >- + Backup bank prepared for { fw_banner_repr }. + Active unchanged; marker { fw_marker }. + when: nbq2_payload_obj is defined + + - name: Bandaid | Pause before Publish success journal to control queue via RabbitMQ HTTP API + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish success journal to control queue via RabbitMQ HTTP API + 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_journal_success_resp + changed_when: (rmq_journal_success_resp.json is defined) and (rmq_journal_success_resp.json.routed | default(false) | bool) + failed_when: > + (rmq_journal_success_resp.status != 200) or + (rmq_journal_success_resp.json is not defined) or + (not (rmq_journal_success_resp.json.routed | default(false) | bool)) + delegate_to: localhost + + - name: Scheduler | Publish action_state done (success) (success) + 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': 'action_state', + 'task_result': 'done' + } | to_json }}" + payload_encoding: "string" + changed_when: false + delegate_to: localhost + when: is_run_by_effective == 'scheduler' + + rescue: + - name: Build failure task name and detail + ansible.builtin.set_fact: + fail_task_name: "{{ ansible_failed_task.name | default('unknown step') }}" + fail_detail_raw: >- + {{ ansible_failed_result.msg + | default(ansible_failed_result.stderr) + | default(ansible_failed_result.stdout) + | default('no additional error output') + | trim }} + + - name: Build failure summary text + ansible.builtin.set_fact: + fail_summary: >- + Firmware update aborted at '{{ fail_task_name }}': {{ fail_detail_raw }} + + - name: Truncate failure summary to ~400 chars + ansible.builtin.set_fact: + fail_summary_short: "{{ fail_summary | regex_replace('\\s+', ' ') | trim | truncate(400, True, '...') }}" + + - name: Build control queue payload for failure journal + ansible.builtin.set_fact: + journal_failure_payload: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "journal_add" + task_result: "{{ fail_summary_short }}" + + - name: Bandaid | Pause before Publish failure journal to control queue via RabbitMQ HTTP API + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish failure journal to control queue via RabbitMQ HTTP API + 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_failure_payload | to_json }}" + payload_encoding: "string" + register: rmq_journal_fail_resp + changed_when: (rmq_journal_fail_resp.json is defined) and (rmq_journal_fail_resp.json.routed | default(false) | bool) + failed_when: > + (rmq_journal_fail_resp.status != 200) or + (rmq_journal_fail_resp.json is not defined) or + (not (rmq_journal_fail_resp.json.routed | default(false) | bool)) + delegate_to: localhost + + - name: Build control queue payload for update-aborted tag + ansible.builtin.set_fact: + tag_failed_payload: + inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" + task_name: "tag_add" + task_result: "update-aborted" + + - name: Bandaid | Pause before Publish update-aborted tag to control queue via RabbitMQ HTTP API + ansible.builtin.pause: + seconds: 1 + delegate_to: localhost + changed_when: false + - name: Publish update-aborted tag to control queue via RabbitMQ HTTP API + 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_failed_payload | to_json }}" + payload_encoding: "string" + register: rmq_tag_failed_resp + changed_when: (rmq_tag_failed_resp.json is defined) and (rmq_tag_failed_resp.json.routed | default(false) | bool) + failed_when: > + (rmq_tag_failed_resp.status != 200) or + (rmq_tag_failed_resp.json is not defined) or + (not (rmq_tag_failed_resp.json.routed | default(false) | bool)) + delegate_to: localhost + + - name: Scheduler | Publish action_state failed (update failure) + 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': 'action_state', + 'task_result': 'failed' + } | to_json }}" + payload_encoding: "string" + changed_when: false + delegate_to: localhost + when: is_run_by_effective == 'scheduler' + + - name: Scheduler | Debug action_state failed published (update failure) + ansible.builtin.debug: + msg: "scheduler-run detected; published action_state=failed (update failure)" + when: is_run_by_effective == 'scheduler' \ No newline at end of file