- name: Indoor DEV2 cloud-agent bounce via DEV1 → LLDP/tunnel → DEV2 (connection logic preserved; bootenv removed) 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 (or reachable directly via LLDP 10.x) 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=30" # ---------------- 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) }}" # ---------------- Debugging toggle ---------------- debugging: true # ---------------- DEV2 connection (decided early) ---------------- # "tunnel" (default) or "direct_lldp" dev2_conn_method: "tunnel" dev2_ssh_host: "" dev2_ssh_port: "" 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 and defaults delegate_to: localhost ansible.builtin.debug: msg: - "inventory_hostname={{ inventory_hostname }}" - "rmq_host={{ rmq_host }}" - "rmq_port={{ rmq_port }}" - "rmq_vhost={{ rmq_vhost }}" - "rmq_exchange={{ rmq_exchange }}" - "control_queue={{ control_queue }}" - 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: # ============================ LLDP-FIRST CONNECTION DECISION ============================ - name: Compute hostname digits key for LLDP lookup (DEV2) ansible.builtin.set_fact: dev2_lldp_digits: "{{ (inventory_hostname | string) | regex_replace('[^0-9]', '') }}" changed_when: false - name: Discover DEV2 candidate IP via LLDP on DEV1 ansible.builtin.raw: > {{ pathprefix }} DIGITS="{{ dev2_lldp_digits }}"; cat /var/run/lldp_server.json 2>/dev/null \ | grep "${DIGITS}" -A 10 \ | grep address \ | grep -vE 'subtype|ipv6' \ | awk -F'"' '{ print $4 }' \ | head -n1 register: dev2_lldp_ip_raw changed_when: false failed_when: false - name: Capture LLDP-derived DEV2 IP (if any) ansible.builtin.set_fact: lldp_dev2_ip: "{{ (dev2_lldp_ip_raw.stdout | default('')) | trim }}" changed_when: false - name: Classify LLDP candidate range delegate_to: localhost ansible.builtin.set_fact: lldp_ip_class: >- {% set ip = (lldp_dev2_ip | default('')) %} {% if ip == '' %}none {% elif ip.startswith('10.') %}10 {% elif ip.startswith('192.168.') %}192_168 {% else %}other{% endif %} changed_when: false - name: Debug LLDP candidate and classification when: debugging | bool delegate_to: localhost ansible.builtin.debug: msg: - "LLDP digits={{ dev2_lldp_digits | default('') }}" - "LLDP candidate IP={{ lldp_dev2_ip | default('') }}" - "LLDP class={{ lldp_ip_class | default('none') }}" # -------------------- CHANGE 1: override tunnel target from LLDP for 192.168.x.x -------------------- - name: Override dev2_host from LLDP when candidate is 192.168.x.x (for tunnel target) when: (lldp_ip_class | trim) == "192_168" and (lldp_dev2_ip | trim | length > 0) delegate_to: localhost ansible.builtin.set_fact: dev2_host: "{{ lldp_dev2_ip | trim }}" changed_when: false - name: Set connection method to tunnel by default delegate_to: localhost ansible.builtin.set_fact: dev2_conn_method: "tunnel" changed_when: false - name: Switch to direct LLDP mode for 10.x.x.x when: (lldp_ip_class | trim) == "10" delegate_to: localhost ansible.builtin.set_fact: dev2_conn_method: "direct_lldp" changed_when: false - name: Debug connection method decision when: debugging | bool delegate_to: localhost ansible.builtin.debug: msg: - "dev2_conn_method={{ dev2_conn_method }}" - "lldp_dev2_ip={{ lldp_dev2_ip | default('') }}" # ============================ DIRECT LLDP AUTH (10.x) ============================ - name: Try DEV2 login via direct LLDP IP with 'basicpass' (10.x) when: dev2_conn_method == "direct_lldp" delegate_to: localhost ansible.builtin.shell: | set -e HOST="{{ lldp_dev2_ip }}" sshpass -f basicpass ssh \ -o AddressFamily=inet \ -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ -o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \ -o ConnectTimeout=30 \ "{{ dev2_ssh_user }}@${HOST}" echo OK >/dev/null 2>&1 args: executable: /bin/bash register: dev2_lldp_try_basicpass changed_when: false ignore_errors: true - name: Select 'basicpass' for direct LLDP if previous login succeeded when: dev2_conn_method == "direct_lldp" and dev2_lldp_try_basicpass.rc == 0 delegate_to: localhost ansible.builtin.set_fact: dev2_passfile_used: "basicpass" changed_when: false - name: Try DEV2 login via direct LLDP IP with 'basicpass2' (10.x, only if first failed) when: dev2_conn_method == "direct_lldp" and (dev2_passfile_used is not defined) delegate_to: localhost ansible.builtin.shell: | set -e HOST="{{ lldp_dev2_ip }}" sshpass -f basicpass2 ssh \ -o AddressFamily=inet \ -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ -o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \ -o ConnectTimeout=30 \ "{{ dev2_ssh_user }}@${HOST}" echo OK >/dev/null 2>&1 args: executable: /bin/bash register: dev2_lldp_try_basicpass2 changed_when: false ignore_errors: true - name: Select 'basicpass2' for direct LLDP if second login succeeded when: dev2_conn_method == "direct_lldp" and dev2_passfile_used is not defined and dev2_lldp_try_basicpass2.rc == 0 delegate_to: localhost ansible.builtin.set_fact: dev2_passfile_used: "basicpass2" changed_when: false - name: Mark DEV2 auth as NONE for direct LLDP if both attempts failed when: dev2_conn_method == "direct_lldp" and dev2_passfile_used is not defined delegate_to: localhost ansible.builtin.set_fact: dev2_passfile_used: "NONE" changed_when: false - name: Set direct LLDP DEV2 SSH host/port (if auth succeeded) when: dev2_conn_method == "direct_lldp" and dev2_passfile_used != "NONE" delegate_to: localhost ansible.builtin.set_fact: dev2_ssh_host: "{{ lldp_dev2_ip }}" dev2_ssh_port: 22 changed_when: false # ============================ TUNNEL PREP (DEV1 temp IP + tunnel) ============================ - name: Add temporary IP on DEV1 (tolerate 'File exists') when: dev2_conn_method == "tunnel" 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: Discover DEV2 MAC via bridge fdb on DEV1 (best-effort) when: dev2_conn_method == "tunnel" 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) when: dev2_conn_method == "tunnel" 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) when: dev2_conn_method == "tunnel" 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) when: dev2_conn_method == "tunnel" and (dev2_mac | default('') | length > 0) 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: Note skipping static ARP add (no MAC discovered) when: dev2_conn_method == "tunnel" and (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" - name: Pick a free local TCP port for the tunnel (controller side) when: dev2_conn_method == "tunnel" 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 when: dev2_conn_method == "tunnel" and (pick_port.stdout | trim | length) == 0 ansible.builtin.meta: end_host - name: Record chosen local port and create control dir for SSH ControlMaster when: dev2_conn_method == "tunnel" 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 when: dev2_conn_method == "tunnel" delegate_to: localhost ansible.builtin.set_fact: _ctrl_sock: "{{ _ctrl_dir }}/ssh_tunnel_ctl" - name: Start SSH ControlMaster and forward 127.0.0.1:local_port → DEV2:22 via DEV1 when: dev2_conn_method == "tunnel" 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: Probe TCP reachability to DEV2 through the tunnel (nc) when: dev2_conn_method == "tunnel" 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: Stop if tunnel TCP probe failed when: dev2_conn_method == "tunnel" and nc_probe.rc != 0 ansible.builtin.meta: end_host - name: Pick DEV2 password for root (tunnel) try basicpass when: dev2_conn_method == "tunnel" 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 args: executable: /bin/bash register: dev2_try_basicpass changed_when: false ignore_errors: true - name: Select 'basicpass' if previous login succeeded (tunnel) when: dev2_conn_method == "tunnel" and 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, tunnel) when: dev2_conn_method == "tunnel" 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 args: executable: /bin/bash register: dev2_try_basicpass2 changed_when: false ignore_errors: true - name: Select 'basicpass2' if second login succeeded (tunnel) when: dev2_conn_method == "tunnel" and 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 (tunnel) when: dev2_conn_method == "tunnel" and dev2_passfile_used is not defined delegate_to: localhost ansible.builtin.set_fact: dev2_passfile_used: "NONE" changed_when: false - name: Set DEV2 SSH host/port for tunnel mode (if auth succeeded) when: dev2_conn_method == "tunnel" and dev2_passfile_used != "NONE" delegate_to: localhost ansible.builtin.set_fact: dev2_ssh_host: "127.0.0.1" dev2_ssh_port: "{{ _local_port }}" changed_when: false # -------------------- CHANGE 2: safety guard using dev2_mac (only if we actually discovered one) -------------------- - name: Read remote eth0 MAC via selected connection (guard ensure this is DEV2) when: dev2_conn_method == "tunnel" and dev2_passfile_used != "NONE" and (dev2_mac | default('') | trim | length > 0) delegate_to: localhost ansible.builtin.shell: | set -e HOST="{{ dev2_ssh_host }}" PORT="{{ dev2_ssh_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 }}@${HOST}" \ "cat /sys/class/net/eth0/address 2>/dev/null || echo" args: executable: /bin/bash register: dev2_eth0_mac_read changed_when: false - name: Abort if remote eth0 MAC != discovered DEV2 MAC when: dev2_conn_method == "tunnel" and dev2_passfile_used != "NONE" and (dev2_mac | default('') | trim | length > 0) and ((dev2_eth0_mac_read.stdout | default('') | trim | lower) != (dev2_mac | trim | lower)) ansible.builtin.fail: msg: > Safety stop: tunnel reached wrong device. expected_dev2_mac={{ dev2_mac | trim }}, remote_eth0_mac={{ dev2_eth0_mac_read.stdout | default('') | trim }}. # ============================ DEV2 HOSTNAME GUARD ============================ - name: Stop and journal if DEV2 auth failed (no passfile worked) when: dev2_passfile_used == "NONE" block: - name: Build control queue payload for indoor aborted journal (auth failure) ansible.builtin.set_fact: journal_indoor_aborted: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- indoor: aborted: DEV2 auth failed (basicpass/basicpass2 did not work). conn_method={{ dev2_conn_method }} delegate_to: localhost - name: Publish indoor aborted journal (auth failure) ansible.builtin.uri: url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" method: POST user: "{{ rmq_user }}" password: "{{ rmq_pass }}" force_basic_auth: true status_code: 200 headers: content-type: "application/json" body_format: json body: properties: content_type: "application/json" routing_key: "{{ control_queue }}" payload: "{{ journal_indoor_aborted | to_json }}" payload_encoding: "string" register: rmq_journal_indoor_aborted_auth_resp changed_when: (rmq_journal_indoor_aborted_auth_resp.json is defined) and (rmq_journal_indoor_aborted_auth_resp.json.routed | default(false) | bool) failed_when: > (rmq_journal_indoor_aborted_auth_resp.status != 200) or (rmq_journal_indoor_aborted_auth_resp.json is not defined) or (not (rmq_journal_indoor_aborted_auth_resp.json.routed | default(false) | bool)) delegate_to: localhost - name: Stop host after DEV2 auth failure ansible.builtin.meta: end_host - name: Read DEV2 hostname via selected connection (busybox-safe) when: dev2_passfile_used != "NONE" delegate_to: localhost ansible.builtin.shell: | set -e HOST="{{ dev2_ssh_host }}" PORT="{{ dev2_ssh_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 }}@${HOST}" \ "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: 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: Build control queue payload for indoor aborted journal (hostname mismatch) ansible.builtin.set_fact: journal_indoor_aborted: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- indoor: aborted: Hostname mismatch: DEV2={{ _dev2_hn }}, inventory={{ _inv_hn }}, DEV1={{ _dev1_hn }} 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 # ============================ JOURNAL: START ============================ - name: Build control queue payload for 'indoor start' journal (cloud-agent bounce) ansible.builtin.set_fact: journal_indoor_start: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- Indoor: DEV2 reachable and hostname verified; starting cloud-agent bounce. conn_method={{ dev2_conn_method }}, dev2={{ dev2_ssh_host }}:{{ dev2_ssh_port }} 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)) delegate_to: localhost # ============================ CLOUD-AGENT BOUNCE (DEV2) ============================ - name: Move /tmp/launchd/services/cloud-agent to /root/ on DEV2 delegate_to: localhost ansible.builtin.shell: | set -e HOST="{{ dev2_ssh_host }}" PORT="{{ dev2_ssh_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 }}@${HOST}" \ "set -e; PATH=/sbin:/usr/sbin:/bin:/usr/bin:\$PATH; mv -f /tmp/launchd/services/cloud-agent /root/" args: executable: /bin/bash register: move_out changed_when: true - name: Wait 3s before restoring ansible.builtin.pause: seconds: 3 - name: Move /root/cloud-agent back to /tmp/launchd/services/ on DEV2 delegate_to: localhost ansible.builtin.shell: | set -e HOST="{{ dev2_ssh_host }}" PORT="{{ dev2_ssh_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 }}@${HOST}" \ "set -e; PATH=/sbin:/usr/sbin:/bin:/usr/bin:\$PATH; mv -f /root/cloud-agent /tmp/launchd/services/" args: executable: /bin/bash register: move_back changed_when: true - name: Build 'indoor updated' journal payload (cloud-agent bounced) ansible.builtin.set_fact: journal_indoor_updated: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- Indoor: cloud-agent bounced successfully. conn_method={{ dev2_conn_method }}, move_out_rc={{ move_out.rc | default('NA') }}, move_back_rc={{ move_back.rc | default('NA') }} 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)) delegate_to: localhost # ---------------------------- Final operator summary (one-liners) ---------------------------- - name: Summary key outcomes (one-liners) delegate_to: localhost ansible.builtin.debug: msg: - "dev2_conn_method={{ dev2_conn_method }}" - "dev2_ssh_host={{ dev2_ssh_host | default('') }}" - "dev2_ssh_port={{ dev2_ssh_port | default('') }}" - "dev2_passfile_used={{ dev2_passfile_used }}" - "dev2_hostname={{ _dev2_hn | default('') }}" - "bounce_move_out_rc={{ move_out.rc | default('NA') }}" - "bounce_move_back_rc={{ move_back.rc | default('NA') }}" 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) when: dev2_conn_method == "tunnel" 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) when: dev2_conn_method == "tunnel" 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') when: dev2_conn_method == "tunnel" 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 when: dev2_conn_method == "tunnel" and del_ip is defined 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 }}"