Files
ansible-worker/files/ansible-playbooks/sot-updater.yml
2025-10-24 22:12:33 +03:00

619 lines
25 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.

---
# sot-updater.yml — Read Dev1 & Dev2 firmware and publish to controls → netbox-reporter
# Run: nbplay sot-updater.yml <device>
- name: Read fw on Dev1 + Dev2, publish NetBox custom fields (full base, AIRPINGs, soft-fail telemetry)
hosts: all
gather_facts: no
vars:
# Busybox-safe PATH prefix for *all* raw calls on DEV1
pathprefix: "PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; "
# DEV1 credentials (controller-side)
dev1_user: "root"
dev1_pass: "wavewave"
ssh_timeout: 30
# DEV2 behind DEV1
dev2_host: "192.168.1.1"
dev2_port: 22
dev2_ssh_user: "root"
dev2_passfiles: [ "basicpass", "basicpass2" ]
# DEV1 side addressing to reach DEV2
dev2_side_ip: "192.168.1.11/24" # IP we add/remove on DEV1
dev1_iface: "br-wan" # DEV1 interface where IP is added
arping_iface: "eth0" # iface used for ARP refreshes (as per update-indoor)
dev2_side_ip_addr: "{{ dev2_side_ip.split('/')[0] }}"
# Controller SSH common opts
ssh_opts_common: >-
-o PreferredAuthentications=password
-o PubkeyAuthentication=no
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o NumberOfPasswordPrompts=1
-o ConnectTimeout=30
# Rabbit (controls)
rmq_host: "{{ lookup('env','RMQ_HOST') | default('10.210.12.2', true) }}"
rmq_port: "{{ lookup('env','RMQ_PORT') | default('15672', true) }}"
rmq_user: "{{ lookup('env','RMQ_USER') | default('admin', true) }}"
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:
# ───────────────────────── Progress: start ─────────────────────────
- name: "⚙️ Start | Dev1 banner → tunnel/auth → Dev2 firmux (telemetry mode)"
debug:
msg:
- "Device: {{ inventory_hostname }}"
- "Mode: report-only (soft-fail; publish journals on failures)"
# ---------------------------- Temp IP on DEV1 ----------------------------
- name: Add temporary IP on DEV1 (tolerate 'File exists'; soft-fail)
ansible.builtin.raw: >
{{ pathprefix }}
ip a add {{ dev2_side_ip }} dev {{ dev1_iface }}
register: add_ip
changed_when: add_ip.rc == 0
failed_when: false
ignore_errors: true
- name: Soft-report if temp IP add failed (not just 'File exists')
when: add_ip.rc is defined and add_ip.rc != 0 and
('File exists' not in (add_ip.stdout | default(''))) and
('File exists' not in (add_ip.stderr | default('')))
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'journal_entry',
'task_result': 'temp_ip_add_failed',
'task_add1': 'rc=' ~ (add_ip.rc | string),
'task_add2': (add_ip.stderr | default(add_ip.stdout) | trim)[:512]
} | to_json }}"
payload_encoding: "string"
- name: Debug temp IP add
debug:
msg:
- "rc={{ add_ip.rc | default('') }}"
- "stdout={{ (add_ip.stdout | default('')) | trim }}"
- "stderr={{ (add_ip.stderr | default('')) | trim }}"
# ---------------------------- AIRPING #1 (early refresh) ----------------------------
- name: Refresh ARP #1 on DEV1 LAN (unsolicited)
ansible.builtin.raw: >
{{ pathprefix }}
arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3
register: arping1
changed_when: false
failed_when: false
ignore_errors: true
# ---------------------------- Local tunnel prep ----------------------------
- name: Pick a free local TCP port for the tunnel (controller)
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
# --- replace the single failing "Record chosen local port & ControlMaster path" task with these ---
- name: Record chosen local port
delegate_to: localhost
set_fact:
_local_port: "{{ (pick_port.stdout | default('') | trim) }}"
- name: Create ControlMaster socket dir (mktemp -d)
delegate_to: localhost
ansible.builtin.shell: "mktemp -d"
register: mktemp_dir
changed_when: false
failed_when: false
ignore_errors: true
- name: Set _ctrl_dir fact (from mktemp) or leave empty
delegate_to: localhost
set_fact:
_ctrl_dir: "{{ (mktemp_dir.stdout | default('') | trim) }}"
- name: Journal if mktemp failed (no ctrl_dir)
when: _ctrl_dir | length == 0
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'journal_entry',
'task_result': 'mktemp_dir_failed',
'task_add1': (mktemp_dir.stderr | default(mktemp_dir.stdout) | trim)[:256]
} | to_json }}"
payload_encoding: "string"
- name: Compose ControlMaster socket path (only if we have a dir)
when: _ctrl_dir | length > 0
delegate_to: localhost
set_fact:
_ctrl_sock: "{{ _ctrl_dir }}/ssh_tunnel_ctl"
- name: Journal if no free local port was found (continue anyway)
when: _local_port | length == 0
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'journal_entry',
'task_result': 'tunnel_port_pick_failed'
} | to_json }}"
payload_encoding: "string"
# ---------------------------- AIRPING #2 (before tunnel) ----------------------------
- name: Refresh ARP #2 on DEV1 LAN (pre-tunnel)
when: _local_port | length > 0
ansible.builtin.raw: >
{{ pathprefix }}
arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3
register: arping2
changed_when: false
failed_when: false
ignore_errors: true
# ---------------------------- Start tunnel via DEV1 ----------------------------
- name: Start SSH ControlMaster and forward 127.0.0.1:local → DEV2:22 via DEV1
when: _local_port | length > 0
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
failed_when: false
ignore_errors: true
- name: Probe TCP reachability to DEV2 through the tunnel (nc)
when: _local_port | length > 0
delegate_to: localhost
ansible.builtin.shell: "nc -z -w5 127.0.0.1 {{ _local_port }}"
register: nc_probe
changed_when: false
failed_when: false
ignore_errors: true
- name: Set tunnel_ok fact
delegate_to: localhost
set_fact:
tunnel_ok: "{{ (_local_port | length > 0) and (nc_probe.rc is defined) and (nc_probe.rc == 0) }}"
- name: Journal if tunnel probe failed
when: not tunnel_ok
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'journal_entry',
'task_result': 'tunnel_probe_failed',
'task_add1': 'port=' ~ (_local_port | default('n/a'))
} | to_json }}"
payload_encoding: "string"
# ───────────────────────── DEV1: banner → fw_version ─────────────────────────
- name: Dev1 | Probe banner via SSH from controller (classic extraction)
delegate_to: localhost
ansible.builtin.shell: |
set -e
USER="{{ dev1_user }}"
HOST="{{ ansible_host | default(inventory_hostname) }}"
sshpass -p '{{ dev1_pass }}' \
ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -o ConnectTimeout={{ ssh_timeout }} \
"${USER}@${HOST}" \
"PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; cat /etc/banner | grep -i rev | head -n1"
register: dev1_banner
changed_when: false
failed_when: false
ignore_errors: true
# (1) Show what we got from banner so we can see what were parsing
- name: Dev1 | Debug raw banner
when: dev1_banner is defined
delegate_to: localhost
debug:
msg:
- "DEV1 banner raw: {{ (dev1_banner.stdout | default('') | trim) | regex_replace('\n',' ') }}"
# (2) Strip timestamp/anything after a pipe
- name: Dev1 | Strip trailing ' | ...'
delegate_to: localhost
set_fact:
dev1_no_ts: "{{ (dev1_banner.stdout | default('') | trim) | regex_replace('\\|.*$', '') }}"
# (3) Convert 'rev YYYY' to '-rYYYY' (case-insensitive, using explicit flag)
- name: Dev1 | Convert 'rev YYYY' → '-rYYYY'
delegate_to: localhost
set_fact:
dev1_rev_dash: "{{ dev1_no_ts | regex_replace('rev\\.?\\s*([0-9]+)\\s*$', '-r\\1', ignorecase=True) }}"
# (4) Extract the canonical core X.X.X-rYYYY (or empty if not found)
- name: Dev1 | Extract X.X.X-rYYYY core
delegate_to: localhost
set_fact:
dev1_fw_clean: "{{ dev1_rev_dash | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+-r[0-9]+') | default('', true) }}"
- name: Journal if DEV1 normalization failed (no core)
when: (dev1_fw_clean | default('') | length) == 0
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'journal_entry',
'task_result': 'dev1_banner_parse_failed',
'task_add1': (dev1_banner.stdout | default('NO_STDOUT') | trim)[:256]
} | to_json }}"
payload_encoding: "string"
- name: Publish → controls | custom_field_set fw_version (Dev1)
delegate_to: localhost
ansible.builtin.uri:
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': 'fw_version',
'task_result': (dev1_fw_clean if (dev1_fw_clean | length) > 0 else 'unavailable')
} | to_json }}"
payload_encoding: "string"
register: dev1_publish_resp
changed_when: false
# ───────────────────────── DEV2 auth pick ─────────────────────────
- name: Try DEV2 login with 'basicpass' (root)
when: tunnel_ok | default(false)
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f basicpass ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=30 \
-p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1
register: dev2_try_basicpass
changed_when: false
failed_when: false
ignore_errors: true
- name: Select 'basicpass' if previous login succeeded
when: tunnel_ok | default(false) and dev2_try_basicpass.rc == 0
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "basicpass"
- name: Try DEV2 login with 'basicpass2' (only if first failed)
when: tunnel_ok | default(false) and dev2_passfile_used is not defined
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f basicpass2 ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=30 \
-p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1
register: dev2_try_basicpass2
changed_when: false
failed_when: false
ignore_errors: true
- name: Select 'basicpass2' if previous login succeeded
when: tunnel_ok | default(false) and (dev2_passfile_used is not defined) and (dev2_try_basicpass2.rc | default(1)) == 0
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "basicpass2"
- name: Mark DEV2 auth as NONE if both attempts failed
when: tunnel_ok | default(false) and (dev2_passfile_used is not defined)
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "NONE"
- name: Journal if DEV2 auth failed
when: tunnel_ok | default(false) and (dev2_passfile_used == 'NONE')
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'journal_entry',
'task_result': 'dev2_auth_failed'
} | to_json }}"
payload_encoding: "string"
# ---------------------------- AIRPING #3 (post-tunnel stabilizer) ----------------------------
- name: Refresh ARP #3 on DEV1 LAN (post-tunnel)
when: tunnel_ok | default(false)
ansible.builtin.raw: >
{{ pathprefix }}
arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3
register: arping3
changed_when: false
failed_when: false
ignore_errors: true
# ───────────────────────── DEV2: firmux → indoor_fwver ─────────────────────────
- name: Dev2 | Read /usr/lib/release/firmux (if authenticated)
when: tunnel_ok | default(false) and dev2_passfile_used is defined and dev2_passfile_used != 'NONE'
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=30 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"cat /usr/lib/release/firmux 2>/dev/null || true"
args: { executable: /bin/bash }
register: dev2_firmux
changed_when: false
failed_when: false
ignore_errors: true
# Show what we got from DEV2 so parsing is debuggable
- name: Dev2 | Debug raw firmux
when: dev2_firmux is defined
delegate_to: localhost
debug:
msg:
- "DEV2 firmux raw: {{ (dev2_firmux.stdout | default('') | trim) | regex_replace('\n',' | ') }}"
# Build the list of firmux lines (CRLF-safe)
- name: Dev2 | Build firmux lines list
delegate_to: localhost
set_fact:
_firmux_lines: "{{ (dev2_firmux.stdout | default('') | regex_replace('\r','')) | split('\n') | map('trim') | list }}"
# Choose the line (prefer one with 'rev NNNN'); fall back to first non-empty
- name: Dev2 | Choose firmux line (prefer containing 'rev NNNN')
delegate_to: localhost
set_fact:
dev2_line: >-
{{
(_firmux_lines | select('match','(?i).*\\brev\\s*\\d+.*') | list | first)
| default((_firmux_lines | select('truthy') | list | first) | default(''))
}}
- name: Dev2 | Normalize to EXACT 'X.X.X rev YYYY' (lowercase 'rev', single space)
when: tunnel_ok | default(false)
delegate_to: localhost
set_fact:
dev2_line_clean: "{{ (dev2_line | default('') | trim) | regex_replace('\\s*\\|.*$', '') }}"
dev2_ver: "{{ dev2_line_clean | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+') | default('') }}"
dev2_rev_token: "{{ dev2_line_clean | regex_search('(?i)\\brev\\.?\\s*[0-9]+') | default('') }}"
dev2_rev_num: "{{ dev2_rev_token | regex_replace('(?i).*\\brev\\.?\\s*', '') }}"
indoor_fw_norm: "{{ (dev2_ver ~ ' rev ' ~ dev2_rev_num) if (dev2_ver|length>0 and dev2_rev_num|length>0) else '' }}"
- name: Journal if DEV2 normalization failed (no version or rev)
when: tunnel_ok | default(false) and (indoor_fw_norm | default('') | length) == 0
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'journal_entry',
'task_result': 'dev2_firmux_parse_failed',
'task_add1': (dev2_firmux.stdout | default('NO_STDOUT') | trim)[:256]
} | to_json }}"
payload_encoding: "string"
- name: Publish → controls | custom_field_set indoor_fwver (Dev2)
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'custom_field_set',
'task_add1': 'indoor_fwver',
'task_result': (
indoor_fw_norm if (tunnel_ok | default(false)) and (indoor_fw_norm | default('') | length) > 0
else ('unreachable' if not (tunnel_ok | default(false)) else 'unavailable')
)
} | to_json }}"
payload_encoding: "string"
register: dev2_publish_resp
changed_when: false
post_tasks:
- name: "✅ Progress | Cleanup start (close tunnel & remove temp IP)"
debug:
msg: "Closing ControlMaster and removing temporary IP"
# ---------------------------- AIRPING #4 (pre-cleanup) ----------------------------
- name: Refresh ARP #4 on DEV1 LAN (pre-cleanup)
ansible.builtin.raw: >
{{ pathprefix }}
arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3
register: arping4
changed_when: false
failed_when: false
ignore_errors: true
- 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
failed_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'; soft-fail)
ansible.builtin.raw: >
{{ pathprefix }}
ip a del {{ dev2_side_ip }} dev {{ dev1_iface }}
register: del_ip
changed_when: del_ip.rc == 0
failed_when: false
ignore_errors: true
- name: Journal if temp IP delete failed (not just 'Cannot assign requested address')
when: del_ip.rc is defined and del_ip.rc != 0 and
('Cannot assign requested address' not in (del_ip.stdout | default(''))) and
('Cannot assign requested address' not in (del_ip.stderr | default('')))
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': (ansible_hostname | default(inventory_hostname)),
'task_name': 'journal_entry',
'task_result': 'temp_ip_del_failed',
'task_add1': 'rc=' ~ (del_ip.rc | string),
'task_add2': (del_ip.stderr | default(del_ip.stdout) | trim)[:512]
} | to_json }}"
payload_encoding: "string"
- name: "✅ Completed | Device processed (Dev1+Dev2)."
debug:
msg:
- "Device: {{ inventory_hostname }}"
- "Status: DONE (report-only)"