This commit is contained in:
2025-10-22 17:20:02 +03:00
parent 6d883ce59c
commit 235187b5f6

View File

@@ -1,6 +1,6 @@
--- ---
# update-secondline.yml # update-secondline.yml
- name: Second-line upgrade tunneled noninvasive engine - name: Second-line indoor upgrade via DEV1 → tunnel → DEV2 (non-invasive control path)
hosts: all hosts: all
gather_facts: no gather_facts: no
@@ -29,7 +29,7 @@
# SSH options used from controller # 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" 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 noninvasive we do not run update -w here # Image to stage on DEV2 (we validate first; the actual write happens later)
image_filename: "fox200-2.2.1-r6801.bin" image_filename: "fox200-2.2.1-r6801.bin"
image_md5: "56b7211709de617e058b98d4204e2562" image_md5: "56b7211709de617e058b98d4204e2562"
# Optional SHA256; leave empty to skip SHA256 checks # Optional SHA256; leave empty to skip SHA256 checks
@@ -52,21 +52,21 @@
pre_tasks: pre_tasks:
# ------------------------------- Hostname sanity DEV1 ------------------------------- # ------------------------------- Hostname sanity DEV1 -------------------------------
- name: Read DEV1 hostname from env busybox safe - name: Read DEV1 hostname (busybox-safe)
ansible.builtin.raw: > ansible.builtin.raw: >
{{ pathprefix }} {{ pathprefix }}
(cat /proc/sys/kernel/hostname 2>/dev/null || echo "") (cat /proc/sys/kernel/hostname 2>/dev/null || echo "")
register: dev1_host_read register: dev1_host_read
changed_when: false changed_when: false
- name: Stop if DEV1 connected hostname differs from inventory - name: Stop early if connected DEV1 hostname != inventory
ansible.builtin.meta: end_host ansible.builtin.meta: end_host
when: (dev1_host_read.stdout | trim | length > 0) and when: (dev1_host_read.stdout | trim | length > 0) and
((dev1_host_read.stdout | trim) != (inventory_hostname | string)) ((dev1_host_read.stdout | trim) != (inventory_hostname | string))
tasks: tasks:
# ---------------------------- Idempotent temp IP on DEV1 ---------------------------- # ---------------------------- Idempotent temp IP on DEV1 ----------------------------
- name: Add temporary IP on DEV1 idempotent treat File exists as OK - name: Add temporary IP on DEV1 (tolerate 'File exists')
ansible.builtin.raw: > ansible.builtin.raw: >
{{ pathprefix }} {{ pathprefix }}
ip a add {{ dev2_side_ip }} dev {{ dev1_iface }} ip a add {{ dev2_side_ip }} dev {{ dev1_iface }}
@@ -77,7 +77,7 @@
and ('File exists' not in (add_ip.stdout | default(''))) and ('File exists' not in (add_ip.stdout | default('')))
and ('File exists' not in (add_ip.stderr | default(''))) and ('File exists' not in (add_ip.stderr | default('')))
- name: Debug add ip results - name: Debug: result of adding temp IP to DEV1
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
- "add_ip.rc={{ add_ip.rc | default('') }}" - "add_ip.rc={{ add_ip.rc | default('') }}"
@@ -85,19 +85,19 @@
- "add_ip.stderr={{ (add_ip.stderr | default('')) | trim }}" - "add_ip.stderr={{ (add_ip.stderr | default('')) | trim }}"
# ---------------------------- Discover MAC via bridge FDB and add static ARP on DEV1 ---------------------------- # ---------------------------- Discover MAC via bridge FDB and add static ARP on DEV1 ----------------------------
- name: Read dynamic MAC on eth0 behind {{ dev1_iface }} via bridge fdb - name: Discover DEV2 MAC via bridge fdb on DEV1 (best-effort)
ansible.builtin.raw: > ansible.builtin.raw: >
{{ pathprefix }} {{ pathprefix }}
bridge fdb show {{ dev1_iface }} | grep eth0 | grep -v permanent | grep master | awk '{print $1}' | head -n1 bridge fdb show {{ dev1_iface }} | grep eth0 | grep -v permanent | grep master | awk '{print $1}' | head -n1
register: dev2_mac_scan register: dev2_mac_scan
changed_when: false changed_when: false
- name: Set dev2_mac from fdb scan (if any) - name: Capture discovered DEV2 MAC (if any)
ansible.builtin.set_fact: ansible.builtin.set_fact:
dev2_mac: "{{ (dev2_mac_scan.stdout | default('') ) | trim }}" dev2_mac: "{{ (dev2_mac_scan.stdout | default('') ) | trim }}"
changed_when: false changed_when: false
- name: Remove current ARP entry for DEV2 on DEV1 (best-effort) - name: Clear existing ARP entry for DEV2 on DEV1 (best-effort)
ansible.builtin.raw: > ansible.builtin.raw: >
{{ pathprefix }} {{ pathprefix }}
ip neigh del {{ dev2_host }} dev {{ dev1_iface }} 2>/dev/null || true ip neigh del {{ dev2_host }} dev {{ dev1_iface }} 2>/dev/null || true
@@ -105,7 +105,7 @@
changed_when: false changed_when: false
failed_when: false failed_when: false
- name: Add static ARP for DEV2 on DEV1 - name: Add static ARP entry on DEV1 (locks DEV2 IP → discovered MAC)
ansible.builtin.raw: > ansible.builtin.raw: >
{{ pathprefix }} {{ pathprefix }}
ip neigh add {{ dev2_host }} lladdr {{ dev2_mac }} dev {{ dev1_iface }} nud permanent ip neigh add {{ dev2_host }} lladdr {{ dev2_mac }} dev {{ dev1_iface }} nud permanent
@@ -116,7 +116,7 @@
and ('File exists' not in (dev2_arp_add.stdout | default(''))) and ('File exists' not in (dev2_arp_add.stdout | default('')))
and ('File exists' not in (dev2_arp_add.stderr | default(''))) and ('File exists' not in (dev2_arp_add.stderr | default('')))
- name: Debug static ARP result - name: Debug: ARP action summary on DEV1
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
- "dev2_mac={{ dev2_mac | default('UNSET') }}" - "dev2_mac={{ dev2_mac | default('UNSET') }}"
@@ -124,13 +124,13 @@
- "arp_add.out={{ (dev2_arp_add.stdout | default('')) | trim }}" - "arp_add.out={{ (dev2_arp_add.stdout | default('')) | trim }}"
- "arp_add.err={{ (dev2_arp_add.stderr | default('')) | trim }}" - "arp_add.err={{ (dev2_arp_add.stderr | default('')) | trim }}"
- name: Skip ARP add because MAC not found - name: Note: skipping static ARP add (no MAC discovered)
when: dev2_mac is not defined or dev2_mac | length == 0 when: dev2_mac is not defined or dev2_mac | length == 0
ansible.builtin.debug: ansible.builtin.debug:
msg: "No suitable dynamic MAC found via bridge fdb; skipping static ARP add on DEV1" msg: "No suitable dynamic MAC found via bridge fdb; skipping static ARP add on DEV1"
# ---------------------------- Local tunnel preparation ---------------------------- # ---------------------------- Local tunnel preparation ----------------------------
- name: Pick a free local port for the tunnel controller - name: Pick a free local TCP port for the tunnel (controller side)
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
set -e set -e
@@ -153,22 +153,22 @@
register: pick_port register: pick_port
changed_when: false changed_when: false
- name: Stop if no free local port - name: Stop if no free local port was found
ansible.builtin.meta: end_host ansible.builtin.meta: end_host
when: (pick_port.stdout | trim | length) == 0 when: (pick_port.stdout | trim | length) == 0
- name: Record chosen local port and control directory - name: Record chosen local port and create control dir for SSH ControlMaster
delegate_to: localhost delegate_to: localhost
ansible.builtin.set_fact: ansible.builtin.set_fact:
_local_port: "{{ pick_port.stdout | trim }}" _local_port: "{{ pick_port.stdout | trim }}"
_ctrl_dir: "{{ lookup('ansible.builtin.pipe', 'mktemp -d') }}" _ctrl_dir: "{{ lookup('ansible.builtin.pipe', 'mktemp -d') }}"
- name: Build tunnel control socket path - name: Build path for SSH ControlMaster socket
delegate_to: localhost delegate_to: localhost
ansible.builtin.set_fact: ansible.builtin.set_fact:
_ctrl_sock: "{{ _ctrl_dir }}/ssh_tunnel_ctl" _ctrl_sock: "{{ _ctrl_dir }}/ssh_tunnel_ctl"
- name: Debug chosen tunnel params - name: Debug: tunnel parameters (controller side)
delegate_to: localhost delegate_to: localhost
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
@@ -176,11 +176,11 @@
- "ctrl_dir={{ _ctrl_dir }}" - "ctrl_dir={{ _ctrl_dir }}"
- "ctrl_sock={{ _ctrl_sock }}" - "ctrl_sock={{ _ctrl_sock }}"
- name: issue arping with own IP - name: Refresh ARP on DEV1’s LAN (send unsolicited ARP from temporary IP)
ansible.builtin.raw: arping -U -I eth0 192.168.1.11 -c 3 ansible.builtin.raw: arping -U -I eth0 192.168.1.11 -c 3
# ---------------------------- Start SSH local forward via DEV1 ---------------------------- # ---------------------------- Start SSH local forward via DEV1 ----------------------------
- name: Start SSH tunnel via DEV1 background ControlMaster - name: Start SSH ControlMaster and forward 127.0.0.1:local_port → DEV2:22 via DEV1
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
set -e set -e
@@ -195,13 +195,13 @@
register: start_tunnel register: start_tunnel
changed_when: true changed_when: true
- name: Small delay for tunnel to settle - name: Wait a moment for tunnel to settle
delegate_to: localhost delegate_to: localhost
ansible.builtin.wait_for: ansible.builtin.wait_for:
timeout: 1 timeout: 1
changed_when: false changed_when: false
- name: Verify tunnel master running proper O check with destination - name: Verify ControlMaster is running (ssh -O check)
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
set -e set -e
@@ -210,7 +210,7 @@
register: tun_check register: tun_check
changed_when: false changed_when: false
- name: Debug tunnel check - name: Debug: ControlMaster status
delegate_to: localhost delegate_to: localhost
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
@@ -218,7 +218,7 @@
- "tunnel_check.out={{ (tun_check.stdout | default('')) | trim }}" - "tunnel_check.out={{ (tun_check.stdout | default('')) | trim }}"
# ---------------------------- Controller-side sanity for DEV2 auth ---------------------------- # ---------------------------- Controller-side sanity for DEV2 auth ----------------------------
- name: Sanity confirm tunnel TCP reachability to DEV2 - name: Probe TCP reachability to DEV2 through the tunnel (nc)
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
set -e set -e
@@ -227,7 +227,7 @@
changed_when: false changed_when: false
ignore_errors: true ignore_errors: true
- name: Debug reachability result - name: Debug: tunnel reachability result
delegate_to: localhost delegate_to: localhost
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
@@ -235,11 +235,11 @@
- "nc.stdout={{ (nc_probe.stdout | default('')) | trim }}" - "nc.stdout={{ (nc_probe.stdout | default('')) | trim }}"
- "nc.stderr={{ (nc_probe.stderr | default('')) | trim }}" - "nc.stderr={{ (nc_probe.stderr | default('')) | trim }}"
- name: Stop if tunnel TCP check failed - name: Stop if tunnel TCP probe failed
ansible.builtin.meta: end_host ansible.builtin.meta: end_host
when: nc_probe.rc != 0 when: nc_probe.rc != 0
- name: Sanity show presence and permissions of passfiles on controller - name: Show passfiles available on controller (ls)
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
set -e set -e
@@ -247,15 +247,15 @@
register: dev2_ls register: dev2_ls
changed_when: false changed_when: false
- name: Debug passfiles listing - name: Debug: passfiles presence
delegate_to: localhost delegate_to: localhost
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
- "{{ (dev2_ls.stdout | default('')) | trim }}" - "{{ (dev2_ls.stdout | default('')) | trim }}"
- "{{ (dev2_ls.stderr | default('')) | trim }}" - "{{ (dev2_ls.stderr | default('')) | trim }}"
# ---------------------------- Pick DEV2 password root only ---------------------------- # ---------------------------- Pick DEV2 password for root ----------------------------
- name: Try DEV2 login with basicpass root - name: Try DEV2 login with 'basicpass' (root)
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
set -e set -e
@@ -270,14 +270,14 @@
changed_when: false changed_when: false
ignore_errors: true ignore_errors: true
- name: Select basicpass as working passfile - name: Select 'basicpass' if previous login succeeded
when: dev2_try_basicpass.rc == 0 when: dev2_try_basicpass.rc == 0
delegate_to: localhost delegate_to: localhost
ansible.builtin.set_fact: ansible.builtin.set_fact:
dev2_passfile_used: "basicpass" dev2_passfile_used: "basicpass"
changed_when: false changed_when: false
- name: Try DEV2 login with basicpass2 root only if first failed - name: Try DEV2 login with 'basicpass2' (only if first failed)
when: dev2_passfile_used is not defined when: dev2_passfile_used is not defined
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -293,21 +293,21 @@
changed_when: false changed_when: false
ignore_errors: true ignore_errors: true
- name: Select basicpass2 as working passfile - name: Select 'basicpass2' if second login succeeded
when: dev2_passfile_used is not defined and dev2_try_basicpass2.rc == 0 when: dev2_passfile_used is not defined and dev2_try_basicpass2.rc == 0
delegate_to: localhost delegate_to: localhost
ansible.builtin.set_fact: ansible.builtin.set_fact:
dev2_passfile_used: "basicpass2" dev2_passfile_used: "basicpass2"
changed_when: false changed_when: false
- name: Set dev2_passfile_used to NONE if neither worked - name: Mark DEV2 auth as NONE if both attempts failed
when: dev2_passfile_used is not defined when: dev2_passfile_used is not defined
delegate_to: localhost delegate_to: localhost
ansible.builtin.set_fact: ansible.builtin.set_fact:
dev2_passfile_used: "NONE" dev2_passfile_used: "NONE"
changed_when: false changed_when: false
- name: Debug selected DEV2 passfile or NONE - name: Debug: selected DEV2 passfile
delegate_to: localhost delegate_to: localhost
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
@@ -333,13 +333,13 @@
register: dev2_host_read register: dev2_host_read
changed_when: false changed_when: false
- name: Normalize hostnames for strict compare - name: Normalize hostnames for strict compare (inventory/DEV1/DEV2)
ansible.builtin.set_fact: ansible.builtin.set_fact:
_inv_hn: "{{ (inventory_hostname | string) | trim | regex_replace('\\r+$','') | lower }}" _inv_hn: "{{ (inventory_hostname | string) | trim | regex_replace('\\r+$','') | lower }}"
_dev1_hn: "{{ (dev1_host_read.stdout | default('')) | 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 }}" _dev2_hn: "{{ (dev2_host_read.stdout | default('')) | trim | regex_replace('\\r+$','') | lower }}"
- name: Debug hostname bytes (hex) - name: Debug: normalized hostnames (JSON-escaped)
delegate_to: localhost delegate_to: localhost
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
@@ -348,7 +348,7 @@
- "dev2={{ _dev2_hn | tojson }}" - "dev2={{ _dev2_hn | tojson }}"
# ---------------- Hostname equality guard (soft-journal and stop) ---------------- # ---------------- Hostname equality guard (soft-journal and stop) ----------------
- name: Enforce DEV2 hostname equals inventory and DEV1 - name: Guard: DEV2 hostname must equal inventory AND DEV1 (prevents IP churn mistakes)
block: block:
- name: Fail if DEV2 hostname differs from inventory/DEV1 - name: Fail if DEV2 hostname differs from inventory/DEV1
ansible.builtin.fail: ansible.builtin.fail:
@@ -365,12 +365,12 @@
_prep_blocked: false _prep_blocked: false
delegate_to: localhost delegate_to: localhost
- name: Append hostname mismatch to journal - name: Append hostname mismatch info to journal
ansible.builtin.set_fact: ansible.builtin.set_fact:
_journal: "{{ _journal + [ 'Hostname mismatch: DEV2=' ~ _dev2_hn ~ ', inventory=' ~ _inv_hn ~ ', DEV1=' ~ _dev1_hn ] }}" _journal: "{{ _journal + [ 'Hostname mismatch: DEV2=' ~ _dev2_hn ~ ', inventory=' ~ _inv_hn ~ ', DEV1=' ~ _dev1_hn ] }}"
delegate_to: localhost delegate_to: localhost
- name: Build control queue payload for indoor aborted journal (hostname) - name: Build control queue payload for indoor aborted journal (hostname check)
ansible.builtin.set_fact: ansible.builtin.set_fact:
journal_indoor_aborted: journal_indoor_aborted:
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
@@ -379,7 +379,7 @@
indoor: update aborted with following reason(s): {{ (_journal | default([])) | join('; ') }} indoor: update aborted with following reason(s): {{ (_journal | default([])) | join('; ') }}
delegate_to: localhost delegate_to: localhost
- name: Publish indoor aborted journal to control queue (hostname) - name: Publish indoor aborted journal (hostname mismatch)
ansible.builtin.uri: ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST method: POST
@@ -408,17 +408,17 @@
ansible.builtin.meta: end_host ansible.builtin.meta: end_host
# ---------------------------- SOFT-FAIL JOURNAL INIT + PREP MARKER CHECK ---------------------------- # ---------------------------- SOFT-FAIL JOURNAL INIT + PREP MARKER CHECK ----------------------------
- name: Initialize journal and flags - name: Init soft-fail journal flags
ansible.builtin.set_fact: ansible.builtin.set_fact:
_journal: [] _journal: []
_prep_blocked: false _prep_blocked: false
_blocked: false _blocked: false
- name: Build specific marker filename (no .bin) - name: Build specific image marker path on DEV2 (/tmp/prepared_for_<image>)
ansible.builtin.set_fact: ansible.builtin.set_fact:
_marker_specific: "/tmp/prepared_for_{{ image_filename | regex_replace('\\.bin$','') }}" _marker_specific: "/tmp/prepared_for_{{ image_filename | regex_replace('\\.bin$','') }}"
- name: Check for any preparation markers on DEV2 (wildcard count) - name: Count existing preparation markers on DEV2 (best-effort)
when: dev2_passfile_used != "NONE" when: dev2_passfile_used != "NONE"
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -437,7 +437,7 @@
changed_when: false changed_when: false
ignore_errors: true ignore_errors: true
- name: Soft-block if any prep markers already present - 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) 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: ansible.builtin.set_fact:
_prep_blocked: true _prep_blocked: true
@@ -445,7 +445,7 @@
_journal: "{{ _journal + [ 'Preparation markers already present on DEV2 (count=' ~ (dev2_prep_count.stdout | trim) ~ '). Skipping staging/write' ] }}" _journal: "{{ _journal + [ 'Preparation markers already present on DEV2 (count=' ~ (dev2_prep_count.stdout | trim) ~ '). Skipping staging/write' ] }}"
# ---------------------------- DEV2 version firmux primary check ---------------------------- # ---------------------------- DEV2 version firmux primary check ----------------------------
- name: Read DEV2 firmux release - name: Read DEV2 /usr/lib/release/firmux (if present)
when: dev2_passfile_used != "NONE" when: dev2_passfile_used != "NONE"
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -464,14 +464,14 @@
changed_when: false changed_when: false
ignore_errors: true ignore_errors: true
- name: Debug firmux content DEV2 - name: Debug: DEV2 firmux banner (if any)
when: dev2_firmux is defined when: dev2_firmux is defined
delegate_to: localhost delegate_to: localhost
ansible.builtin.debug: ansible.builtin.debug:
msg: "DEV2 firmux={{ (dev2_firmux.stdout | default('')) | trim }}" msg: "DEV2 firmux={{ (dev2_firmux.stdout | default('')) | trim }}"
# ---------------------------- Normalize rebootin early (HOURS) ---------------------------- # ---------------------------- Normalize rebootin early (HOURS) ----------------------------
- name: "Normalize rebootin (phase 1: raw/is_now/is_int/HOURS)" - name: Normalize 'rebootin' (detect 'now' or integer HOURS)
delegate_to: localhost delegate_to: localhost
ansible.builtin.set_fact: ansible.builtin.set_fact:
_reboot_raw: "{{ rebootin | default('') | string | trim | lower }}" _reboot_raw: "{{ rebootin | default('') | string | trim | lower }}"
@@ -483,15 +483,10 @@
else (rebootin | int if ((rebootin | default('') | string | trim) is match('^\\d+$') ) else -1) else (rebootin | int if ((rebootin | default('') | string | trim) is match('^\\d+$') ) else -1)
}} }}
- name: "Normalize rebootin (phase 2: requested flag)" - name: Compute reboot flags (requested? seconds? minutes?)
delegate_to: localhost delegate_to: localhost
ansible.builtin.set_fact: ansible.builtin.set_fact:
_reboot_requested: "{{ _reboot_is_now or _reboot_is_int }}" _reboot_requested: "{{ _reboot_is_now or _reboot_is_int }}"
# Compute effective seconds from HOURS (0h → ~20 seconds grace); clamp non-negative
- name: "Normalize rebootin (phase 3: compute seconds from HOURS)"
delegate_to: localhost
ansible.builtin.set_fact:
_reboot_seconds: >- _reboot_seconds: >-
{{ {{
20 20
@@ -501,28 +496,31 @@
if (_reboot_requested | default(false)) if (_reboot_requested | default(false))
else 0 else 0
}} }}
# Derive minutes from seconds (non-negative), for tasks/summary that expect minutes
- name: "Normalize rebootin (phase 4: derive minutes)"
delegate_to: localhost
ansible.builtin.set_fact:
_reboot_minutes: "{{ ((_reboot_seconds | int) // 60) if (_reboot_seconds | int) > 0 else 0 }}" _reboot_minutes: "{{ ((_reboot_seconds | int) // 60) if (_reboot_seconds | int) > 0 else 0 }}"
# ---------------------------- Journal: indoor start (after we know we can proceed) ---------------------------- - name: Debug: reboot plan summary
- name: Build control queue payload for indoor start journal 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: ansible.builtin.set_fact:
journal_indoor_start: journal_indoor_start:
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
task_name: "journal_add" task_name: "journal_add"
task_result: >- task_result: >-
Indoor: Dev2 is reachable, starting with update. Indoor: Dev2 is reachable, starting update.
image={{ image_filename }}, md5={{ image_md5 }}, image={{ image_filename }}, md5={{ image_md5 }},
firmux={{ (dev2_firmux.stdout | default('unknown')) | trim }}, firmux={{ (dev2_firmux.stdout | default('unknown')) | trim }},
reboot={{ 'in ' ~ (_reboot_seconds | int) ~ ' seconds' if (_reboot_is_now and (_reboot_minutes | int) == 0) else (rebootin | default('') | trim) }} reboot={{ 'in ' ~ (_reboot_seconds | int) ~ ' seconds' if (_reboot_is_now and (_reboot_minutes | int) == 0) else (rebootin | default('') | trim) }}
when: dev2_passfile_used != "NONE" when: dev2_passfile_used != "NONE"
delegate_to: localhost delegate_to: localhost
- name: Publish indoor start journal to control queue - name: Publish 'indoor start' journal to control queue
ansible.builtin.uri: ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST method: POST
@@ -548,20 +546,20 @@
when: journal_indoor_start is defined when: journal_indoor_start is defined
delegate_to: localhost delegate_to: localhost
# ---------------------------- Stage image on DEV2 no write ---------------------------- # ---------------------------- Stage image on DEV2 (no write yet) ----------------------------
- name: Check local presence of image file - name: Check local presence of image file on controller
delegate_to: localhost delegate_to: localhost
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ image_filename }}" path: "{{ image_filename }}"
register: local_img register: local_img
- name: Soft-block if image missing locally - name: Soft-block if local image is missing
when: not local_img.stat.exists when: not local_img.stat.exists
ansible.builtin.set_fact: ansible.builtin.set_fact:
_blocked: true _blocked: true
_journal: "{{ _journal + [ 'Local image missing on controller: ' ~ image_filename ] }}" _journal: "{{ _journal + [ 'Local image missing on controller: ' ~ image_filename ] }}"
- name: Compute local md5 of the image - name: Compute local md5 of the image (controller)
when: local_img.stat.exists when: local_img.stat.exists
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -590,7 +588,7 @@
changed_when: false changed_when: false
ignore_errors: true ignore_errors: true
- name: Soft-block if local sha256 mismatch or unavailable - 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)) 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: ansible.builtin.set_fact:
_prep_blocked: true _prep_blocked: true
@@ -598,7 +596,7 @@
_journal: "{{ _journal + [ 'Local sha256 mismatch/unavailable: have=' ~ ((local_sha256.stdout | default('NA')) | trim) ~ ' expected=' ~ (image_sha256 | trim) ] }}" _journal: "{{ _journal + [ 'Local sha256 mismatch/unavailable: have=' ~ ((local_sha256.stdout | default('NA')) | trim) ~ ' expected=' ~ (image_sha256 | trim) ] }}"
# fw_printenv health before upload (soft-fail) # fw_printenv health before upload (soft-fail)
- name: Read fw_printenv size (line count) on DEV2 - 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)) when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -616,14 +614,14 @@
changed_when: false changed_when: false
ignore_errors: true ignore_errors: true
- name: Soft-block if fw_printenv too small - 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) 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: ansible.builtin.set_fact:
_prep_blocked: true _prep_blocked: true
_blocked: true _blocked: true
_journal: "{{ _journal + [ 'fw_printenv too small on DEV2: ' ~ (dev2_fwenv_wc.stdout | trim) ~ ' lines (<30). Skipping image staging' ] }}" _journal: "{{ _journal + [ 'fw_printenv too small on DEV2: ' ~ (dev2_fwenv_wc.stdout | trim) ~ ' lines (<30). Skipping image staging' ] }}"
- name: Check existing image md5 on DEV2 NOFILE if absent - name: Check existing DEV2 image md5 (NOFILE if missing)
when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false)) when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -640,7 +638,7 @@
register: dev2_md5_before register: dev2_md5_before
changed_when: false changed_when: false
- name: Copy image to DEV2 via tunnel only if missing or md5 mismatch - 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) 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 delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -655,7 +653,7 @@
register: scp_push register: scp_push
changed_when: true changed_when: true
- name: Compute md5 of image on DEV2 after copy or if exists - 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)) when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -672,7 +670,7 @@
register: dev2_md5_after register: dev2_md5_after
changed_when: false changed_when: false
- name: Verify DEV2 md5 matches expected (soft-fail journal) - 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) 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: ansible.builtin.set_fact:
_prep_blocked: true _prep_blocked: true
@@ -705,7 +703,7 @@
_blocked: true _blocked: true
_journal: "{{ _journal + [ 'Remote sha256 mismatch/unavailable on DEV2: have=' ~ ((dev2_sha256_after.stdout | default('NA')) | trim) ~ ' expected=' ~ (image_sha256 | trim) ] }}" _journal: "{{ _journal + [ 'Remote sha256 mismatch/unavailable on DEV2: have=' ~ ((dev2_sha256_after.stdout | default('NA')) | trim) ~ ' expected=' ~ (image_sha256 | trim) ] }}"
- name: Run noninvasive update check on DEV2 update c - 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)) when: local_img.stat.exists and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -723,13 +721,13 @@
register: dev2_update_check register: dev2_update_check
changed_when: false changed_when: false
- name: Debug update c output DEV2 - name: Debug: output from 'update -c'
when: dev2_update_check is defined when: dev2_update_check is defined
delegate_to: localhost delegate_to: localhost
ansible.builtin.debug: ansible.builtin.debug:
msg: "{{ (dev2_update_check.stdout | default('')) | trim }}" msg: "{{ (dev2_update_check.stdout | default('')) | trim }}"
- name: Soft-block if update -c did not say valid - 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')) when: dev2_update_check is defined and not ((dev2_update_check.stdout | default('') | lower) is search('valid'))
ansible.builtin.set_fact: ansible.builtin.set_fact:
_prep_blocked: true _prep_blocked: true
@@ -737,7 +735,7 @@
_journal: "{{ _journal + [ 'update -c did not return valid on DEV2; output=' ~ ((dev2_update_check.stdout | default('')) | trim) ] }}" _journal: "{{ _journal + [ 'update -c did not return valid on DEV2; output=' ~ ((dev2_update_check.stdout | default('')) | trim) ] }}"
# ---------------------------- Journal: indoor aborted (if any blockers) ---------------------------- # ---------------------------- Journal: indoor aborted (if any blockers) ----------------------------
- name: Build control queue payload for indoor aborted journal - name: Build control queue payload for 'indoor aborted' journal
ansible.builtin.set_fact: ansible.builtin.set_fact:
journal_indoor_aborted: journal_indoor_aborted:
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
@@ -747,7 +745,7 @@
when: (_blocked | default(false)) when: (_blocked | default(false))
delegate_to: localhost delegate_to: localhost
- name: Publish indoor aborted journal to control queue - name: Publish 'indoor aborted' journal to control queue
ansible.builtin.uri: ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST method: POST
@@ -774,10 +772,10 @@
delegate_to: localhost delegate_to: localhost
# ============================ ACTUAL UPGRADE WRITE + BANK FLIP (only if not blocked) ============================ # ============================ ACTUAL UPGRADE WRITE + BANK FLIP (only if not blocked) ============================
- name: Upgrade write and flip block - name: Upgrade write and bank flip on DEV2 (guarded by soft-block)
when: not (_blocked | default(false)) when: not (_blocked | default(false))
block: block:
- name: Write image on DEV2 (this will take a while) - name: Write image to inactive bank on DEV2 (update -w)
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
set -e set -e
@@ -795,7 +793,7 @@
changed_when: true changed_when: true
failed_when: dev2_up_write.stdout is not search('update is complete') failed_when: dev2_up_write.stdout is not search('update is complete')
- name: Read current active partition on DEV2 - name: Read current active partition on DEV2 (before flip)
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
set -e set -e
@@ -813,12 +811,12 @@
changed_when: false changed_when: false
failed_when: (dev2_active_before.stdout | trim) not in ['1','2'] failed_when: (dev2_active_before.stdout | trim) not in ['1','2']
- name: Determine new active value for DEV2 - 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']) 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: ansible.builtin.set_fact:
dev2_new_active: "{{ '1' if (dev2_active_before.stdout | trim) == '2' else '2' }}" dev2_new_active: "{{ '1' if (dev2_active_before.stdout | trim) == '2' else '2' }}"
- name: Switch active partition to {{ dev2_new_active }} - name: Set fw_setenv active={{ dev2_new_active }} on DEV2
when: dev2_new_active is defined when: dev2_new_active is defined
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -836,7 +834,7 @@
register: dev2_setenv_out register: dev2_setenv_out
changed_when: true changed_when: true
- name: Verify active partition flipped on DEV2 - name: Verify active partition flipped on DEV2 (read back)
when: dev2_new_active is defined when: dev2_new_active is defined
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -855,7 +853,7 @@
changed_when: false changed_when: false
failed_when: (dev2_active_after.stdout | trim) != (dev2_new_active | string) failed_when: (dev2_active_after.stdout | trim) != (dev2_new_active | string)
- name: Create specific prep marker on DEV2 (no generic) - name: Create specific prep marker on DEV2 for this image
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
set -e set -e
@@ -874,7 +872,7 @@
ignore_errors: true ignore_errors: true
# ---------------------------- Reboot scheduling (normalized) ---------------------------- # ---------------------------- Reboot scheduling (normalized) ----------------------------
- name: Schedule delayed reboot on DEV2 minutes - 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)) when: _reboot_requested and (_reboot_minutes | int) >= 0 and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -894,7 +892,7 @@
changed_when: true changed_when: true
ignore_errors: true ignore_errors: true
- name: Build control queue payload for indoor-restart-scheduled tag - name: Build 'indoor-restart-scheduled' tag payload
ansible.builtin.set_fact: ansible.builtin.set_fact:
tag_restart_sched_payload: tag_restart_sched_payload:
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
@@ -903,7 +901,7 @@
when: _reboot_requested and (_reboot_minutes | int) >= 0 and dev2_passfile_used != "NONE" and not (_blocked | default(false)) when: _reboot_requested and (_reboot_minutes | int) >= 0 and dev2_passfile_used != "NONE" and not (_blocked | default(false))
delegate_to: localhost delegate_to: localhost
- name: Publish indoor-restart-scheduled tag to control queue via RabbitMQ HTTP API - name: Publish 'indoor-restart-scheduled' tag to control queue
ansible.builtin.uri: ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST method: POST
@@ -929,19 +927,28 @@
when: tag_restart_sched_payload is defined when: tag_restart_sched_payload is defined
delegate_to: localhost delegate_to: localhost
- name: Note reboot request bad format - name: Note: reboot was requested but value is invalid (format warning)
when: (rebootin | default('') | string | trim | length) > 0 and not _reboot_requested when: (rebootin | default('') | string | trim | length) > 0 and not _reboot_requested
ansible.builtin.debug: ansible.builtin.debug:
msg: "Reboot requested but value '{{ rebootin | string | trim }}' is invalid; not applied" msg: "Reboot requested but value '{{ rebootin | string | trim }}' is invalid; not applied"
# ---------------------------- Journal: indoor updated and reboot schedule ---------------------------- # ---------------------------- Journal: indoor updated and reboot schedule ----------------------------
- name: Build control queue payload for indoor updated journal - name: Compute write success flag (_write_success)
ansible.builtin.set_fact: ansible.builtin.set_fact:
_write_success: "{{ (dev2_up_write.stdout | default('')) is search('update is complete') if (dev2_up_write is defined) else false }}" _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)) when: not (_blocked | default(false))
delegate_to: localhost delegate_to: localhost
- name: Build indoor updated journal payload text - 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: ansible.builtin.set_fact:
journal_indoor_updated: journal_indoor_updated:
inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}"
@@ -958,7 +965,7 @@
when: not (_blocked | default(false)) and (_write_success | bool) when: not (_blocked | default(false)) and (_write_success | bool)
delegate_to: localhost delegate_to: localhost
- name: Publish indoor updated journal to control queue - name: Publish 'indoor updated' journal to control queue
ansible.builtin.uri: ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST method: POST
@@ -984,9 +991,8 @@
when: journal_indoor_updated is defined when: journal_indoor_updated is defined
delegate_to: localhost delegate_to: localhost
# ──────────────────────────────── NEW: derive expected version for the checker ──────────────────────────────── # ──────────────────────────────── Derive expected version for the checker (strict) ────────────────────────────────
# Schedule window (we only send after-upgrade if write succeeded & not blocked) - name: Init after-upgrade scheduling vars (attempt=1, delay=reboot + 6m)
- name: Init after-upgrade scheduling vars (attempt 1 + delay from reboot + 6m)
ansible.builtin.set_fact: ansible.builtin.set_fact:
au_attempt: 1 au_attempt: 1
au_max_attempts: 3 au_max_attempts: 3
@@ -994,15 +1000,14 @@
when: not (_blocked | default(false)) and (_write_success | bool) when: not (_blocked | default(false)) and (_write_success | bool)
delegate_to: localhost delegate_to: localhost
- name: Generate correlation ID and original emitted timestamp - name: Generate correlation ID and UTC timestamp (for after-upgrade tracking)
ansible.builtin.set_fact: ansible.builtin.set_fact:
au_correlation_id: "{{ lookup('pipe', 'date +%s%N | sha1sum | cut -c1-12') }}" au_correlation_id: "{{ lookup('pipe', 'date +%s%N | sha1sum | cut -c1-12') }}"
au_original_emitted_at: "{{ lookup('pipe', 'date -u +%FT%TZ') }}" au_original_emitted_at: "{{ lookup('pipe', 'date -u +%FT%TZ') }}"
when: au_delay_sec is defined when: au_delay_sec is defined
delegate_to: localhost delegate_to: localhost
# Strict, single-source expected version: "X.Y.Z-rNNNN" from image_filename - name: Derive expected target_version from image filename (strict: X.Y.Z-rNNNN)
- name: Compute expected firmware core "X.Y.Z-rNNNN" from image filename (strict)
delegate_to: localhost delegate_to: localhost
ansible.builtin.set_fact: ansible.builtin.set_fact:
expected_fw_core: >- expected_fw_core: >-
@@ -1012,19 +1017,7 @@
}} }}
when: au_delay_sec is defined when: au_delay_sec is defined
- name: Ensure expected_fw_core is set (strict parse from filename) - name: Debug: derived expected target_version for checker
delegate_to: localhost
ansible.builtin.set_fact:
expected_fw_core: >-
{{
expected_fw_core | default(
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 target version
delegate_to: localhost delegate_to: localhost
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
@@ -1047,13 +1040,16 @@
when: au_delay_sec is defined when: au_delay_sec is defined
delegate_to: localhost delegate_to: localhost
- name: Debug x-delay about to be sent (ms) - name: Debug: after-upgrade plan (routing + delay + version)
ansible.builtin.debug: ansible.builtin.debug:
msg: "x-delay(ms) = {{ (au_delay_sec | int) * 1000 }}" 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 when: afterupgrade_payload is defined
delegate_to: localhost delegate_to: localhost
- name: Publish delayed after-upgrade check to holding exchange - name: Publish delayed after-upgrade check (headers.x-delay) to holding exchange
ansible.builtin.uri: ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ 'deviceconfig.delayed' | urlencode }}/publish" url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ 'deviceconfig.delayed' | urlencode }}/publish"
method: POST method: POST
@@ -1080,8 +1076,8 @@
when: afterupgrade_payload is defined when: afterupgrade_payload is defined
delegate_to: localhost delegate_to: localhost
# ---------------------------- summary ---------------------------- # ---------------------------- Final operator summary (concise) ----------------------------
- name: Summary show key results - name: Summary: key outcomes (one-liners)
delegate_to: localhost delegate_to: localhost
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
@@ -1104,28 +1100,28 @@
- "journal={{ (_journal | default([])) | join(' || ') }}" - "journal={{ (_journal | default([])) | join(' || ') }}"
post_tasks: post_tasks:
- name: Cleanup always - name: Cleanup (always)
block: block:
- ansible.builtin.debug: - ansible.builtin.debug:
msg: "entering cleanup block" msg: "Entering cleanup block"
changed_when: false changed_when: false
delegate_to: localhost delegate_to: localhost
always: always:
- name: Close tunnel best effort - name: Close SSH ControlMaster (best-effort)
delegate_to: localhost delegate_to: localhost
ansible.builtin.shell: | ansible.builtin.shell: |
ssh -S "{{ _ctrl_sock | default('/dev/null') }}" -O exit 2>/dev/null || true ssh -S "{{ _ctrl_sock | default('/dev/null') }}" -O exit 2>/dev/null || true
changed_when: false changed_when: false
ignore_errors: true ignore_errors: true
- name: Remove control dir best effort - name: Remove tunnel control dir (best-effort)
delegate_to: localhost delegate_to: localhost
ansible.builtin.file: ansible.builtin.file:
path: "{{ _ctrl_dir | default('/tmp/none') }}" path: "{{ _ctrl_dir | default('/tmp/none') }}"
state: absent state: absent
ignore_errors: true ignore_errors: true
- name: Remove temporary IP on DEV1 idempotent Cannot assign requested address OK - name: Remove temporary IP on DEV1 (tolerate 'Cannot assign requested address')
ansible.builtin.raw: > ansible.builtin.raw: >
{{ pathprefix }} {{ pathprefix }}
ip a del {{ dev2_side_ip }} dev {{ dev1_iface }} ip a del {{ dev2_side_ip }} dev {{ dev1_iface }}
@@ -1136,7 +1132,7 @@
and ('Cannot assign requested address' not in (del_ip.stdout | default(''))) and ('Cannot assign requested address' not in (del_ip.stdout | default('')))
and ('Cannot assign requested address' not in (del_ip.stderr | default(''))) and ('Cannot assign requested address' not in (del_ip.stderr | default('')))
- name: Debug del ip results - name: Debug: temp IP removal result
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
- "del_ip.rc={{ del_ip.rc | default('') }}" - "del_ip.rc={{ del_ip.rc | default('') }}"