715 lines
29 KiB
YAML
715 lines
29 KiB
YAML
- 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('<none>') }}"
|
|
- "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('<none>') }}"
|
|
|
|
# ============================ 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 }}"
|