Files
ansible-worker/files/ansible-playbooks/update-indoor.yml
2025-10-22 22:12:02 +03:00

1182 lines
51 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
# update-secondline.yml
- name: Second-line indoor upgrade via DEV1 → tunnel → DEV2 (non-invasive control path)
hosts: all
gather_facts: no
vars:
# Busybox-safe PATH prefix for all remote raw calls on DEV1
pathprefix: "PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; "
# DEV1 credentials (stable, like rebootin222)
dev1_user: "root"
dev1_pass: "wavewave"
# Tunnel target DEV2 behind DEV1
dev2_host: "192.168.1.1"
dev2_port: 22
# Temp IP we add to DEV1 so it can reach DEV2
dev2_side_ip: "192.168.1.11/24"
dev1_iface: "br-wan"
# DEV2 behind the tunnel
dev2_ssh_user: "root"
dev2_passfiles:
- "basicpass"
- "basicpass2"
# SSH options used from controller
ssh_opts_common: "-o PreferredAuthentications=password -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o NumberOfPasswordPrompts=1 -o ConnectTimeout=15"
# Image to stage on DEV2 (we validate first; the actual write happens later)
image_filename: "fox200-2.2.1-r6801.bin"
image_md5: "56b7211709de617e058b98d4204e2562"
# Optional SHA256; leave empty to skip SHA256 checks
image_sha256: ""
dev2_image_dir: "/tmp"
dev2_image_path: "{{ dev2_image_dir }}/{{ image_filename }}"
# Reboot delay in HOURS (integer). Consumer always passes hours; 0 means immediate (~20s grace).
rebootin: ""
# ---------------- RabbitMQ journaling (mirrors rebootin222 style) ----------------
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) }}"
afterupgrade_routing_key: "{{ lookup('env','AFTERUP_ROUTING_KEY') | default('deviceconfig', true) }}"
pre_tasks:
# ------------------------------- Hostname sanity DEV1 -------------------------------
- name: Read DEV1 hostname (busybox-safe)
ansible.builtin.raw: >
{{ pathprefix }}
(cat /proc/sys/kernel/hostname 2>/dev/null || echo "")
register: dev1_host_read
changed_when: false
- name: Debug incoming parameters from consumer and defaults
delegate_to: localhost
ansible.builtin.debug:
msg:
- "rebootin={{ rebootin | default('UNSET') }}"
- "image_filename={{ image_filename | default('UNSET') }}"
- "image_md5={{ image_md5 | default('UNSET') }}"
- "image_sha256={{ image_sha256 | default('UNSET') }}"
- "afterupgrade_routing_key={{ afterupgrade_routing_key | default('UNSET') }}"
- "inventory_hostname={{ inventory_hostname }}"
- name: Stop early if connected DEV1 hostname != inventory
ansible.builtin.meta: end_host
when: (dev1_host_read.stdout | trim | length > 0) and
((dev1_host_read.stdout | trim) != (inventory_hostname | string))
tasks:
# ---------------------------- Idempotent temp IP on DEV1 ----------------------------
- 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 result of adding temp IP to DEV1
ansible.builtin.debug:
msg:
- "add_ip.rc={{ add_ip.rc | default('') }}"
- "add_ip.stdout={{ (add_ip.stdout | default('')) | trim }}"
- "add_ip.stderr={{ (add_ip.stderr | default('')) | trim }}"
# ---------------------------- Discover MAC via bridge FDB and add static ARP on DEV1 ----------------------------
- name: Discover DEV2 MAC via bridge fdb on DEV1 (best-effort)
ansible.builtin.raw: >
{{ pathprefix }}
bridge fdb show {{ dev1_iface }} | grep eth0 | grep -v permanent | grep master | awk '{print $1}' | head -n1
register: dev2_mac_scan
changed_when: false
- name: Capture discovered DEV2 MAC (if any)
ansible.builtin.set_fact:
dev2_mac: "{{ (dev2_mac_scan.stdout | default('') ) | trim }}"
changed_when: false
- name: Clear existing ARP entry for DEV2 on DEV1 (best-effort)
ansible.builtin.raw: >
{{ pathprefix }}
ip neigh del {{ dev2_host }} dev {{ dev1_iface }} 2>/dev/null || true
register: dev2_arp_del
changed_when: false
failed_when: false
- name: Add static ARP entry on DEV1 (locks DEV2 IP → discovered MAC)
ansible.builtin.raw: >
{{ pathprefix }}
ip neigh add {{ dev2_host }} lladdr {{ dev2_mac }} dev {{ dev1_iface }} nud permanent
register: dev2_arp_add
changed_when: dev2_arp_add.rc == 0
failed_when: >
dev2_arp_add.rc != 0
and ('File exists' not in (dev2_arp_add.stdout | default('')))
and ('File exists' not in (dev2_arp_add.stderr | default('')))
- name: Debug ARP action summary on DEV1
ansible.builtin.debug:
msg:
- "dev2_mac={{ dev2_mac | default('UNSET') }}"
- "arp_add.rc={{ dev2_arp_add.rc | default('') }}"
- "arp_add.out={{ (dev2_arp_add.stdout | default('')) | trim }}"
- "arp_add.err={{ (dev2_arp_add.stderr | default('')) | trim }}"
- name: Note skipping static ARP add (no MAC discovered)
when: dev2_mac is not defined or dev2_mac | length == 0
ansible.builtin.debug:
msg: "No suitable dynamic MAC found via bridge fdb; skipping static ARP add on DEV1"
# ---------------------------- Local tunnel preparation ----------------------------
- name: Pick a free local TCP port for the tunnel (controller side)
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: 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 dir for SSH ControlMaster
delegate_to: localhost
ansible.builtin.set_fact:
_local_port: "{{ pick_port.stdout | trim }}"
_ctrl_dir: "{{ lookup('ansible.builtin.pipe', 'mktemp -d') }}"
- name: Build path for SSH ControlMaster socket
delegate_to: localhost
ansible.builtin.set_fact:
_ctrl_sock: "{{ _ctrl_dir }}/ssh_tunnel_ctl"
- name: Debug tunnel parameters (controller side)
delegate_to: localhost
ansible.builtin.debug:
msg:
- "local_port={{ _local_port }}"
- "ctrl_dir={{ _ctrl_dir }}"
- "ctrl_sock={{ _ctrl_sock }}"
- name: Refresh ARP on DEV1s LAN (send unsolicited ARP from temporary IP)
ansible.builtin.raw: arping -U -I eth0 192.168.1.11 -c 3
# ---------------------------- Start SSH local forward via DEV1 ----------------------------
- name: Start SSH ControlMaster and forward 127.0.0.1:local_port → 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 {{ 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
- name: Wait a moment for tunnel to settle
delegate_to: localhost
ansible.builtin.wait_for:
timeout: 1
changed_when: false
- name: Verify ControlMaster is running (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 ControlMaster status
delegate_to: localhost
ansible.builtin.debug:
msg:
- "tunnel_check.rc={{ tun_check.rc }}"
- "tunnel_check.out={{ (tun_check.stdout | default('')) | trim }}"
# ---------------------------- Controller-side sanity for DEV2 auth ----------------------------
- name: Probe TCP reachability to DEV2 through the tunnel (nc)
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 tunnel reachability result
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 TCP probe failed
ansible.builtin.meta: end_host
when: nc_probe.rc != 0
- name: Show passfiles available on controller (ls)
delegate_to: localhost
ansible.builtin.shell: |
set -e
ls -l basicpass basicpass2 2>/dev/null || echo "no passfiles in CWD"
register: dev2_ls
changed_when: false
- name: Debug passfiles presence
delegate_to: localhost
ansible.builtin.debug:
msg:
- "{{ (dev2_ls.stdout | default('')) | trim }}"
- "{{ (dev2_ls.stderr | default('')) | trim }}"
# ---------------------------- Pick DEV2 password for root ----------------------------
- name: Try DEV2 login with 'basicpass' (root)
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f basicpass ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=15 \
-p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1
register: dev2_try_basicpass
changed_when: false
ignore_errors: true
- name: Select 'basicpass' if previous login succeeded
when: dev2_try_basicpass.rc == 0
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "basicpass"
changed_when: false
- name: Try DEV2 login with 'basicpass2' (only if first failed)
when: dev2_passfile_used is not defined
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f basicpass2 ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=15 \
-p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1
register: dev2_try_basicpass2
changed_when: false
ignore_errors: true
- name: Select 'basicpass2' if second login succeeded
when: dev2_passfile_used is not defined and dev2_try_basicpass2.rc == 0
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "basicpass2"
changed_when: false
- name: Mark DEV2 auth as NONE if both attempts failed
when: dev2_passfile_used is not defined
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "NONE"
changed_when: false
- name: Debug selected DEV2 passfile
delegate_to: localhost
ansible.builtin.debug:
msg:
- "dev2_passfile_used={{ dev2_passfile_used }}"
- "try_basicpass.rc={{ (dev2_try_basicpass.rc | default('NA')) }}"
- "try_basicpass2.rc={{ (dev2_try_basicpass2.rc | default('SKIPPED')) }}"
- name: Read DEV2 hostname via tunnel (busybox-safe)
when: dev2_passfile_used != "NONE"
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=15 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"cat /proc/sys/kernel/hostname 2>/dev/null || hostname || echo"
args:
executable: /bin/bash
register: dev2_host_read
changed_when: false
- name: Normalize hostnames for strict compare (inventory/DEV1/DEV2)
ansible.builtin.set_fact:
_inv_hn: "{{ (inventory_hostname | string) | trim | regex_replace('\\r+$','') | lower }}"
_dev1_hn: "{{ (dev1_host_read.stdout | default('')) | trim | regex_replace('\\r+$','') | lower }}"
_dev2_hn: "{{ (dev2_host_read.stdout | default('')) | trim | regex_replace('\\r+$','') | lower }}"
- name: Debug normalized hostnames (JSON-escaped)
delegate_to: localhost
ansible.builtin.debug:
msg:
- "inv={{ _inv_hn | tojson }}"
- "dev1={{ _dev1_hn | tojson }}"
- "dev2={{ _dev2_hn | tojson }}"
# ---------------- Hostname equality guard (soft-journal and stop) ----------------
- name: Guard DEV2 hostname must equal inventory AND DEV1 (prevents IP churn mistakes)
block:
- name: Fail if DEV2 hostname differs from inventory/DEV1
ansible.builtin.fail:
msg: >
Hostname mismatch: DEV2='{{ _dev2_hn }}',
inventory='{{ _inv_hn }}',
DEV1='{{ _dev1_hn }}'
when: (_dev2_hn != _inv_hn) or (_dev2_hn != _dev1_hn)
rescue:
- name: Initialize journal array for hostname mismatch
ansible.builtin.set_fact:
_journal: []
_blocked: true
_prep_blocked: false
delegate_to: localhost
- name: Append hostname mismatch info to journal
ansible.builtin.set_fact:
_journal: "{{ _journal + [ 'Hostname mismatch: DEV2=' ~ _dev2_hn ~ ', inventory=' ~ _inv_hn ~ ', DEV1=' ~ _dev1_hn ] }}"
delegate_to: localhost
- name: Build control queue payload for indoor aborted journal (hostname check)
ansible.builtin.set_fact:
journal_indoor_aborted:
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
task_name: "journal_add"
task_result: >-
indoor: update aborted with following reason(s): {{ (_journal | default([])) | join('; ') }}
delegate_to: localhost
- name: Publish indoor aborted journal (hostname mismatch)
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_indoor_aborted | to_json }}"
payload_encoding: "string"
register: rmq_journal_indoor_aborted_hn_resp
changed_when: (rmq_journal_indoor_aborted_hn_resp.json is defined) and (rmq_journal_indoor_aborted_hn_resp.json.routed | default(false) | bool)
failed_when: >
(rmq_journal_indoor_aborted_hn_resp.status != 200) or
(rmq_journal_indoor_aborted_hn_resp.json is not defined) or
(not (rmq_journal_indoor_aborted_hn_resp.json.routed | default(false) | bool))
delegate_to: localhost
- name: Stop host after hostname mismatch
ansible.builtin.meta: end_host
# ---------------------------- SOFT-FAIL JOURNAL INIT + PREP MARKER CHECK ----------------------------
- name: Init soft-fail journal flags
ansible.builtin.set_fact:
_journal: []
_prep_blocked: false
_blocked: false
- name: Build specific image marker path on DEV2 (/tmp/prepared_for_<image>)
ansible.builtin.set_fact:
_marker_specific: "/tmp/prepared_for_{{ image_filename | regex_replace('\\.bin$','') }}"
- name: Count existing preparation markers on DEV2 (best-effort)
when: dev2_passfile_used != "NONE"
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=10 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"ls /tmp/prepared_for* 2>/dev/null | wc -l"
args:
executable: /bin/bash
register: dev2_prep_count
changed_when: false
ignore_errors: true
- name: Soft-block if preparation markers already present on DEV2
when: dev2_prep_count is defined and (dev2_prep_count.stdout is defined) and ((dev2_prep_count.stdout | trim | int) > 0)
ansible.builtin.set_fact:
_prep_blocked: true
_blocked: true
_journal: "{{ _journal + [ 'Preparation markers already present on DEV2 (count=' ~ (dev2_prep_count.stdout | trim) ~ '). Skipping staging/write' ] }}"
# ---------------------------- DEV2 version firmux primary check ----------------------------
- name: Read DEV2 /usr/lib/release/firmux (if present)
when: dev2_passfile_used != "NONE"
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=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
ignore_errors: true
- name: Debug DEV2 firmux banner (if any)
when: dev2_firmux is defined
delegate_to: localhost
ansible.builtin.debug:
msg: "DEV2 firmux={{ (dev2_firmux.stdout | default('')) | trim }}"
# ---------------------------- Normalize rebootin early (HOURS) ----------------------------
# Accept: "", "now", "immediate", numeric (4), numeric string ("4"), or "4h"
- name: Normalize rebootin detect now or integer HOURS
delegate_to: localhost
ansible.builtin.set_fact:
_reboot_raw: "{{ rebootin | default('') | string | trim | lower }}"
_reboot_is_now: "{{ (rebootin | default('') | string | trim | lower) in ['', 'now', 'immediate'] }}"
_reboot_is_numtype: "{{ (rebootin | type_debug) in ['int','float'] }}"
_reboot_is_digitstr: "{{ (rebootin | default('') | string | trim) is match('^\\d+$') }}"
_reboot_is_hsuffix: "{{ (rebootin | default('') | string | trim | lower) is match('^\\d+h$') }}"
_reboot_hours: >-
{{
0
if ((rebootin | default('') | string | trim | lower) in ['', 'now', 'immediate'])
else ( (rebootin | int)
if ((rebootin | type_debug) in ['int','float'])
else ( (_reboot_raw | regex_replace('h$', '') | int)
if (_reboot_is_digitstr or _reboot_is_hsuffix)
else -1))
}}
- name: Debug reboot normalization detail
delegate_to: localhost
ansible.builtin.debug:
msg:
- "_reboot_raw={{ _reboot_raw | default('UNSET') }}"
- "_reboot_is_now={{ _reboot_is_now | default('UNSET') }}"
- "_reboot_is_numtype={{ _reboot_is_numtype | default('UNSET') }}"
- "_reboot_is_digitstr={{ _reboot_is_digitstr | default('UNSET') }}"
- "_reboot_is_hsuffix={{ _reboot_is_hsuffix | default('UNSET') }}"
- "_reboot_hours={{ _reboot_hours | default('UNSET') }}"
- name: Warn if rebootin was not provided by consumer (debug only)
when: (rebootin | default('') | string | trim) == ''
delegate_to: localhost
ansible.builtin.debug:
msg: "WARNING: rebootin is empty or missing. Consumer likely did not pass -e rebootin=<hours>."
# Compute reboot flags and *final* seconds.
# Rule: now/empty/0 -> 20s grace; hours>0 -> hours*3600 (no extra 20s).
- name: Compute reboot flags requested and seconds with proper conversion
delegate_to: localhost
ansible.builtin.set_fact:
_reboot_requested: "{{ _reboot_is_now or (_reboot_hours | int) >= 0 }}"
_reboot_seconds: >-
{{
20
if (_reboot_is_now or ((_reboot_hours | int) == 0))
else ((_reboot_hours | int) * 3600)
}}
# Derive minutes from seconds
- name: Derive reboot minutes from seconds
delegate_to: localhost
ansible.builtin.set_fact:
_reboot_minutes: "{{ ((_reboot_seconds | int) // 60) if (_reboot_seconds | int) > 0 else 0 }}"
- name: Debug reboot plan summary
delegate_to: localhost
ansible.builtin.debug:
msg:
- "reboot_requested={{ _reboot_requested | default(false) }}"
- "reboot_seconds={{ _reboot_seconds | default(0) }}"
- "reboot_minutes={{ _reboot_minutes | default(0) }}"
# ---------------------------- Journal: indoor start (we can proceed) ----------------------------
- name: Build control queue payload for 'indoor start' journal
ansible.builtin.set_fact:
journal_indoor_start:
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
task_name: "journal_add"
task_result: >-
Indoor: Dev2 is reachable, starting update.
image={{ image_filename }}, md5={{ image_md5 }},
firmux={{ (dev2_firmux.stdout | default('unknown')) | trim }},
reboot={{ 'in ' ~ (_reboot_seconds | int) ~ ' seconds' if (_reboot_requested | default(false)) else 'not requested' }}
when: dev2_passfile_used != "NONE"
delegate_to: localhost
- name: Publish 'indoor start' journal to control queue
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_indoor_start | to_json }}"
payload_encoding: "string"
register: rmq_journal_indoor_start_resp
changed_when: (rmq_journal_indoor_start_resp.json is defined) and (rmq_journal_indoor_start_resp.json.routed | default(false) | bool)
failed_when: >
(rmq_journal_indoor_start_resp.status != 200) or
(rmq_journal_indoor_start_resp.json is not defined) or
(not (rmq_journal_indoor_start_resp.json.routed | default(false) | bool))
when: journal_indoor_start is defined
delegate_to: localhost
# ---------------------------- Stage image on DEV2 (no write yet) ----------------------------
- name: Check local presence of image file on controller
delegate_to: localhost
ansible.builtin.stat:
path: "{{ image_filename }}"
register: local_img
- name: Soft-block if local image is missing
when: not local_img.stat.exists
ansible.builtin.set_fact:
_blocked: true
_journal: "{{ _journal + [ 'Local image missing on controller: ' ~ image_filename ] }}"
- name: Compute local md5 of the image (controller)
when: local_img.stat.exists
delegate_to: localhost
ansible.builtin.shell: |
set -e
md5sum "{{ image_filename }}" | awk '{print $1}'
register: local_md5
changed_when: false
- name: Verify local md5 matches expected
when: local_img.stat.exists
delegate_to: localhost
ansible.builtin.assert:
that:
- (local_md5.stdout | trim) == image_md5
fail_msg: "Local md5 does not match expected got {{ local_md5.stdout | trim }} expected {{ image_md5 }}"
success_msg: "Local md5 matches expected"
# Optional SHA256 local
- name: Compute local sha256 of the image (if provided)
when: local_img.stat.exists and (image_sha256 | default('') | length) > 0
delegate_to: localhost
ansible.builtin.shell: |
set -e
sha256sum "{{ image_filename }}" | awk '{print $1}'
register: local_sha256
changed_when: false
ignore_errors: true
- name: Soft-block if local sha256 mismatch/unavailable
when: local_img.stat.exists and (image_sha256 | default('') | length) > 0 and (local_sha256 is not defined or (local_sha256.stdout | trim) != (image_sha256 | trim))
ansible.builtin.set_fact:
_prep_blocked: true
_blocked: true
_journal: "{{ _journal + [ 'Local sha256 mismatch/unavailable: have=' ~ ((local_sha256.stdout | default('NA')) | trim) ~ ' expected=' ~ (image_sha256 | trim) ] }}"
# fw_printenv health before upload (soft-fail)
- name: Read fw_printenv size (line count) on DEV2 (soft health)
when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=10 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"(fw_printenv 2>/dev/null | wc -l) || echo 0"
args:
executable: /bin/bash
register: dev2_fwenv_wc
changed_when: false
ignore_errors: true
- name: Soft-block if fw_printenv too small (<30 lines)
when: dev2_fwenv_wc is defined and (dev2_fwenv_wc.stdout is defined) and ((dev2_fwenv_wc.stdout | trim | int) < 30)
ansible.builtin.set_fact:
_prep_blocked: true
_blocked: true
_journal: "{{ _journal + [ 'fw_printenv too small on DEV2: ' ~ (dev2_fwenv_wc.stdout | trim) ~ ' lines (<30). Skipping image staging' ] }}"
- name: Check existing DEV2 image md5 (NOFILE if missing)
when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=15 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"[ -f '{{ dev2_image_path }}' ] && md5sum '{{ dev2_image_path }}' | awk '{print \$1}' || echo NOFILE"
args:
executable: /bin/bash
register: dev2_md5_before
changed_when: false
- name: Copy image to DEV2 if missing or md5 mismatch
when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false)) and ((dev2_md5_before.stdout | trim) != image_md5)
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" scp \
-P "$PORT" \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
"{{ image_filename }}" "{{ dev2_ssh_user }}@127.0.0.1:{{ dev2_image_dir }}/"
args:
executable: /bin/bash
register: scp_push
changed_when: true
- name: Compute md5 of image on DEV2 after copy (or if already present)
when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=15 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"md5sum '{{ dev2_image_path }}' 2>/dev/null | awk '{print \$1}' || echo NOFILE"
args:
executable: /bin/bash
register: dev2_md5_after
changed_when: false
- name: Soft-block if DEV2 md5 != expected
when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false)) and ((dev2_md5_after.stdout | trim) != image_md5)
ansible.builtin.set_fact:
_prep_blocked: true
_blocked: true
_journal: "{{ _journal + [ 'Remote md5 mismatch on DEV2: have=' ~ (dev2_md5_after.stdout | trim) ~ ' expected=' ~ image_md5 ~ '. Skipping further prep' ] }}"
# Optional SHA256 remote
- name: Compute sha256 of image on DEV2 (if provided)
when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false)) and (image_sha256 | default('') | length) > 0
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=10 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"sha256sum '{{ dev2_image_path }}' 2>/dev/null | awk '{print \$1}' || echo NOSHA"
args:
executable: /bin/bash
register: dev2_sha256_after
changed_when: false
ignore_errors: true
- name: Soft-block if DEV2 sha256 mismatch/unavailable
when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false)) and (image_sha256 | default('') | length) > 0 and (dev2_sha256_after is not defined or (dev2_sha256_after.stdout | trim) != (image_sha256 | trim))
ansible.builtin.set_fact:
_prep_blocked: true
_blocked: true
_journal: "{{ _journal + [ 'Remote sha256 mismatch/unavailable on DEV2: have=' ~ ((dev2_sha256_after.stdout | default('NA')) | trim) ~ ' expected=' ~ (image_sha256 | trim) ] }}"
- name: Pre-validate image on DEV2 with 'update -c' (non-invasive)
when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=15 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"update -c '{{ dev2_image_path }}' 2>&1 || true"
args:
executable: /bin/bash
register: dev2_update_check
changed_when: false
- name: Debug output from 'update -c'
when: dev2_update_check is defined
delegate_to: localhost
ansible.builtin.debug:
msg: "{{ (dev2_update_check.stdout | default('')) | trim }}"
- name: Soft-block if 'update -c' did not return 'valid'
when: dev2_update_check is defined and not ((dev2_update_check.stdout | default('') | lower) is search('valid'))
ansible.builtin.set_fact:
_prep_blocked: true
_blocked: true
_journal: "{{ _journal + [ 'update -c did not return valid on DEV2; output=' ~ ((dev2_update_check.stdout | default('')) | trim) ] }}"
# ---------------------------- Journal: indoor aborted (if any blockers) ----------------------------
- name: Build control queue payload for 'indoor aborted' journal
ansible.builtin.set_fact:
journal_indoor_aborted:
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
task_name: "journal_add"
task_result: >-
indoor: update aborted with following reason(s): {{ (_journal | default([])) | join('; ') }}
when: (_blocked | default(false))
delegate_to: localhost
- name: Publish 'indoor aborted' journal to control queue
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_indoor_aborted | to_json }}"
payload_encoding: "string"
register: rmq_journal_indoor_aborted_resp
changed_when: (rmq_journal_indoor_aborted_resp.json is defined) and (rmq_journal_indoor_aborted_resp.json.routed | default(false) | bool)
failed_when: >
(rmq_journal_indoor_aborted_resp.status != 200) or
(rmq_journal_indoor_aborted_resp.json is not defined) or
(not (rmq_journal_indoor_aborted_resp.json.routed | default(false) | bool))
when: journal_indoor_aborted is defined
delegate_to: localhost
# ============================ ACTUAL UPGRADE WRITE + BANK FLIP (only if not blocked) ============================
- name: Upgrade write and bank flip on DEV2 (guarded by soft-block)
when: not (_blocked | default(false))
block:
- name: Write image to inactive bank on DEV2 (update -w)
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=0 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"update -w '{{ dev2_image_path }}'"
args:
executable: /bin/bash
register: dev2_up_write
changed_when: true
failed_when: dev2_up_write.stdout is not search('update is complete')
- name: Read current active partition on DEV2 (before flip)
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=10 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"fw_printenv active | awk -F= '/^active=/{print \$2}'"
args:
executable: /bin/bash
register: dev2_active_before
changed_when: false
failed_when: (dev2_active_before.stdout | trim) not in ['1','2']
- name: Determine new active value for DEV2 (flip 1↔2)
when: dev2_active_before is defined and (dev2_active_before.stdout is defined) and ((dev2_active_before.stdout | trim) in ['1','2'])
ansible.builtin.set_fact:
dev2_new_active: "{{ '1' if (dev2_active_before.stdout | trim) == '2' else '2' }}"
- name: Set fw_setenv active={{ dev2_new_active }} on DEV2
when: dev2_new_active is defined
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=10 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"fw_setenv active {{ dev2_new_active }}"
args:
executable: /bin/bash
register: dev2_setenv_out
changed_when: true
- name: Verify active partition flipped on DEV2 (read back)
when: dev2_new_active is defined
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=10 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"fw_printenv active | awk -F= '/^active=/{print \$2}'"
args:
executable: /bin/bash
register: dev2_active_after
changed_when: false
failed_when: (dev2_active_after.stdout | trim) != (dev2_new_active | string)
- name: Create specific prep marker on DEV2 for this image
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=10 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"touch '{{ _marker_specific }}'"
args:
executable: /bin/bash
register: dev2_marker_write
changed_when: true
ignore_errors: true
# ---------------------------- Reboot scheduling (normalized) ----------------------------
- name: Schedule DEV2 reboot after computed delay (seconds)
when: _reboot_requested and (_reboot_minutes | int) >= 0 and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
SECS="{{ _reboot_seconds | int }}"
CMD='/sbin/reboot -d '"${SECS}"' >/dev/null 2>&1 &'
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=10 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" "${CMD}"
args:
executable: /bin/bash
register: dev2_reboot_sched
changed_when: true
ignore_errors: true
- name: Build 'indoor-restart-scheduled' tag payload
ansible.builtin.set_fact:
tag_restart_sched_payload:
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
task_name: "tag_add"
task_result: "indoor-restart-scheduled"
when: _reboot_requested and (_reboot_minutes | int) >= 0 and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost
- name: Publish 'indoor-restart-scheduled' tag to control queue
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_sched_payload | to_json }}"
payload_encoding: "string"
register: rmq_tag_restart_sched_resp
changed_when: (rmq_tag_restart_sched_resp.json is defined) and (rmq_tag_restart_sched_resp.json.routed | default(false) | bool)
failed_when: >
(rmq_tag_restart_sched_resp.status != 200) or
(rmq_tag_restart_sched_resp.json is not defined) or
(not (rmq_tag_restart_sched_resp.json.routed | default(false) | bool))
when: tag_restart_sched_payload is defined
delegate_to: localhost
- name: Note reboot was requested but value is invalid (format warning)
when: (rebootin | default('') | string | trim | length) > 0 and not _reboot_requested
ansible.builtin.debug:
msg: "Reboot requested but value '{{ rebootin | string | trim }}' is invalid; not applied"
# ---------------------------- Journal: indoor updated and reboot schedule ----------------------------
- name: Compute write success flag (_write_success)
ansible.builtin.set_fact:
_write_success: "{{ (dev2_up_write.stdout | default('')) is search('update is complete') if (dev2_up_write is defined) else false }}"
when: not (_blocked | default(false))
delegate_to: localhost
- name: Debug write result and active banks (quick summary)
when: not (_blocked | default(false))
delegate_to: localhost
ansible.builtin.debug:
msg:
- "write_success={{ _write_success | default(false) }}"
- "active_before={{ (dev2_active_before.stdout | default('NA')) | trim }}"
- "active_after={{ (dev2_active_after.stdout | default('NA')) | trim }}"
- name: Build 'indoor updated' journal payload text
ansible.builtin.set_fact:
journal_indoor_updated:
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
task_name: "journal_add"
task_result: >-
Indoor: Dev2 updated, scheduling reboot {{
('in ' ~ (_reboot_seconds | int) ~ ' seconds') if (_reboot_requested and (_reboot_minutes | int) == 0)
else ('in ' ~ (_reboot_minutes | int) ~ ' minutes') if (_reboot_requested and (_reboot_minutes | int) > 0)
else 'not requested'
}}.
write_done={{ _write_success }},
active_before={{ (dev2_active_before.stdout | default('NA')) | trim }},
active_after={{ (dev2_active_after.stdout | default('NA')) | trim }}
when: not (_blocked | default(false)) and (_write_success | bool)
delegate_to: localhost
- name: Publish 'indoor updated' journal to control queue
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_indoor_updated | to_json }}"
payload_encoding: "string"
register: rmq_journal_indoor_updated_resp
changed_when: (rmq_journal_indoor_updated_resp.json is defined) and (rmq_journal_indoor_updated_resp.json.routed | default(false) | bool)
failed_when: >
(rmq_journal_indoor_updated_resp.status != 200) or
(rmq_journal_indoor_updated_resp.json is not defined) or
(not (rmq_journal_indoor_updated_resp.json.routed | default(false) | bool))
when: journal_indoor_updated is defined
delegate_to: localhost
# ──────────────────────────────── Derive expected version for the checker (strict) ────────────────────────────────
- name: Init after-upgrade scheduling vars (attempt=1, delay=reboot + 6m)
ansible.builtin.set_fact:
au_attempt: 1
au_max_attempts: 3
au_delay_sec: "{{ (_reboot_seconds | int) + 360 }}"
when: not (_blocked | default(false)) and (_write_success | bool)
delegate_to: localhost
- name: Generate correlation ID and UTC timestamp (for after-upgrade tracking)
ansible.builtin.set_fact:
au_correlation_id: "{{ lookup('pipe', 'date +%s%N | sha1sum | cut -c1-12') }}"
au_original_emitted_at: "{{ lookup('pipe', 'date -u +%FT%TZ') }}"
when: au_delay_sec is defined
delegate_to: localhost
- name: Derive expected target_version from image filename (strict X.Y.Z-rNNNN)
delegate_to: localhost
ansible.builtin.set_fact:
expected_fw_core: >-
{{
image_filename
| regex_replace('.*?([0-9]+\\.[0-9]+\\.[0-9]+)-r([0-9]+).*','\\1-r\\2')
}}
when: au_delay_sec is defined
- name: Debug derived expected target_version for checker
delegate_to: localhost
ansible.builtin.debug:
msg:
- "image_filename={{ image_filename }}"
- "expected_fw_core={{ expected_fw_core }}"
when: expected_fw_core is defined
- name: Build after-upgrade check payload (attempt 1)
ansible.builtin.set_fact:
afterupgrade_payload:
task_name: "afterupgrade_indoor_check"
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
target_version: "{{ expected_fw_core }}"
attempt: "{{ au_attempt }}"
max_attempts: "{{ au_max_attempts }}"
current_delay_sec: "{{ au_delay_sec }}"
correlation_id: "{{ au_correlation_id }}"
original_emitted_at: "{{ au_original_emitted_at }}"
schema_version: 1
when: au_delay_sec is defined
delegate_to: localhost
- name: Debug after-upgrade plan (routing + delay + version)
ansible.builtin.debug:
msg:
- "routing_key={{ afterupgrade_routing_key }}"
- "x-delay(ms)={{ (au_delay_sec | int) * 1000 }}"
- "target_version={{ afterupgrade_payload.target_version | default('NA') }}"
when: afterupgrade_payload is defined
delegate_to: localhost
- name: Publish delayed after-upgrade check (headers.x-delay) to holding exchange
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ 'deviceconfig.delayed' | 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"
headers:
x-delay: "{{ (au_delay_sec | int) * 1000 }}"
routing_key: "{{ afterupgrade_routing_key }}"
payload: "{{ afterupgrade_payload | to_json }}"
payload_encoding: "string"
register: rmq_afterupgrade_resp
changed_when: (rmq_afterupgrade_resp.json is defined) and (rmq_afterupgrade_resp.json.routed | default(false) | bool)
failed_when: >
(rmq_afterupgrade_resp.status != 200) or
(rmq_afterupgrade_resp.json is not defined)
when: afterupgrade_payload is defined
delegate_to: localhost
# ---------------------------- Final operator summary (concise) ----------------------------
- name: Summary key outcomes (one-liners)
delegate_to: localhost
ansible.builtin.debug:
msg:
- "dev2_passfile_used={{ dev2_passfile_used }}"
- "dev2_firmux={{ (dev2_firmux.stdout | default('')) | trim }}"
- "local_image_present={{ local_img.stat.exists | default(false) }}"
- "local_md5={{ (local_md5.stdout | default('NA')) | trim }}"
- "dev2_md5_before={{ (dev2_md5_before.stdout | default('NA')) | trim }}"
- "dev2_md5_after={{ (dev2_md5_after.stdout | default('NA')) | trim }}"
- "update_c_len={{ (dev2_update_check.stdout | default('') ) | length }}"
- "write_done={{ (dev2_up_write.stdout | default('')) is search('update is complete') if (dev2_up_write is defined) else 'NA' }}"
- "active_before={{ (dev2_active_before.stdout | default('NA')) | trim }}"
- "active_after={{ (dev2_active_after.stdout | default('NA')) | trim }}"
- "reboot_requested={{ _reboot_requested | default(false) }}"
- "reboot_delay_seconds={{ _reboot_seconds | int if (_reboot_requested | default(false)) else 'NA' }}"
- "reboot_delay_minutes={{ _reboot_minutes | int if (_reboot_requested | default(false)) else 'NA' }}"
- "reboot_applied={{ (dev2_reboot_sched is defined and dev2_reboot_sched.rc is defined and dev2_reboot_sched.rc == 0) | default(false) }}"
- "prep_blocked={{ _prep_blocked | default(false) }}"
- "blocked={{ _blocked | default(false) }}"
- "journal={{ (_journal | default([])) | join(' || ') }}"
post_tasks:
- name: Cleanup (always)
block:
- ansible.builtin.debug:
msg: "Entering cleanup block"
changed_when: false
delegate_to: localhost
always:
- name: Close SSH ControlMaster (best-effort)
delegate_to: localhost
ansible.builtin.shell: |
ssh -S "{{ _ctrl_sock | default('/dev/null') }}" -O exit 2>/dev/null || true
changed_when: false
ignore_errors: true
- name: Remove tunnel control dir (best-effort)
delegate_to: localhost
ansible.builtin.file:
path: "{{ _ctrl_dir | default('/tmp/none') }}"
state: absent
ignore_errors: true
- name: Remove temporary IP on DEV1 (tolerate 'Cannot assign requested address')
ansible.builtin.raw: >
{{ pathprefix }}
ip a del {{ dev2_side_ip }} dev {{ dev1_iface }}
register: del_ip
changed_when: del_ip.rc == 0
failed_when: >
del_ip.rc != 0
and ('Cannot assign requested address' not in (del_ip.stdout | default('')))
and ('Cannot assign requested address' not in (del_ip.stderr | default('')))
- name: Debug temp IP removal result
ansible.builtin.debug:
msg:
- "del_ip.rc={{ del_ip.rc | default('') }}"
- "del_ip.stdout={{ (del_ip.stdout | default('')) | trim }}"
- "del_ip.stderr={{ (del_ip.stderr | default('')) | trim }}"
when: del_ip is defined