This commit is contained in:
2026-01-20 10:11:00 +02:00
parent fc68a3d6f5
commit 1d269d343f

View File

@@ -1,6 +1,8 @@
# ptsd-migration.yml — PPPoE to super DHCP | Phase 0 connectivity + Phase 1 commands (initially uptime)
# ptsd-migration.yml — PPPoE to super DHCP
# Phase 0: connectivity + safe identity guard
# Phase 1: wrapper (no device changes yet; uptime will be introduced later)
- name: "PPPoE to super DHCP | Phase 0 | Prove reliable DEV2 connectivity (no device commands)"
- name: "PPPoE to super DHCP | Phase 0+1 | Reliable DEV2 connectivity + wrapper"
hosts: all
gather_facts: no
@@ -31,6 +33,7 @@
- "basicpass2"
# SSH options used from controller
# LogLevel=ERROR is critical: suppresses hostkey "Permanently added..." lines that poison parsing.
ssh_opts_common: >-
-o PreferredAuthentications=password
-o PubkeyAuthentication=no
@@ -38,6 +41,8 @@
-o UserKnownHostsFile=/dev/null
-o NumberOfPasswordPrompts=1
-o ConnectTimeout=30
-o ConnectionAttempts=1
-o LogLevel=ERROR
# Debugging toggle
debugging: true
@@ -120,7 +125,6 @@
- "LLDP IPv6 candidate={{ lldp_dev2_ip6 | default('<none>') }}"
# ============================ PRIMARY PATH DECISION (same as indoor-restart-cloudagent) ============================
# If LLDP gives 192.168.* we use it as dev2_host for the tunnel target
- name: Override dev2_host from LLDP when candidate is 192.168.x.x
when: (lldp_ip_class | trim) == "192_168" and (lldp_dev2_ip | trim | length > 0)
delegate_to: localhost
@@ -128,7 +132,6 @@
dev2_host: "{{ lldp_dev2_ip | trim }}"
changed_when: false
# Direct LLDP only if 10.x
- name: Set connection method initial (direct_lldp if 10.x else tunnel)
delegate_to: localhost
ansible.builtin.set_fact:
@@ -144,7 +147,6 @@
HOST="{{ lldp_dev2_ip }}"
sshpass -f basicpass ssh {{ ssh_opts_common }} \
-o AddressFamily=inet \
-o ConnectionAttempts=1 \
"{{ dev2_ssh_user }}@${HOST}" echo OK >/dev/null 2>&1
args: { executable: /bin/bash }
register: dev2_lldp_try_basicpass
@@ -166,7 +168,6 @@
HOST="{{ lldp_dev2_ip }}"
sshpass -f basicpass2 ssh {{ ssh_opts_common }} \
-o AddressFamily=inet \
-o ConnectionAttempts=1 \
"{{ dev2_ssh_user }}@${HOST}" echo OK >/dev/null 2>&1
args: { executable: /bin/bash }
register: dev2_lldp_try_basicpass2
@@ -327,7 +328,6 @@
PORT="{{ _local_port }}"
sshpass -f basicpass ssh {{ ssh_opts_common }} \
-o AddressFamily=inet \
-o ConnectionAttempts=1 \
-p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1
args: { executable: /bin/bash }
register: dev2_try_basicpass
@@ -349,7 +349,6 @@
PORT="{{ _local_port }}"
sshpass -f basicpass2 ssh {{ ssh_opts_common }} \
-o AddressFamily=inet \
-o ConnectionAttempts=1 \
-p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1
args: { executable: /bin/bash }
register: dev2_try_basicpass2
@@ -370,33 +369,56 @@
dev2_passfile_used_tunnel: "NONE"
changed_when: false
# Safety guard (only if we discovered MAC)
- name: Read remote eth0 MAC via tunnel (guard)
when: dev2_conn_method == "tunnel" and dev2_passfile_used_tunnel != "NONE" and (dev2_mac | default('') | trim | length > 0)
# ============================ SAFETY GUARD: MAC CHECK (only if tunnel auth succeeded) ============================
- name: Read remote eth0 MAC via tunnel (guard, sanitized)
when:
- dev2_conn_method == "tunnel"
- (dev2_passfile_used_tunnel | default('NONE')) != "NONE"
- (dev2_mac | default('') | length) > 0
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used_tunnel }}" ssh {{ ssh_opts_common }} \
-o AddressFamily=inet \
-o ConnectionAttempts=1 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"cat /sys/class/net/eth0/address 2>/dev/null || echo" 2>&1
"cat /sys/class/net/eth0/address 2>/dev/null || ip link show eth0 2>/dev/null" 2>&1
args: { executable: /bin/bash }
register: dev2_eth0_mac_read
register: dev2_eth0_mac_raw
changed_when: false
failed_when: false
- name: Normalize remote eth0 MAC (extract last MAC-like token)
when:
- dev2_conn_method == "tunnel"
- (dev2_passfile_used_tunnel | default('NONE')) != "NONE"
- (dev2_mac | default('') | length) > 0
delegate_to: localhost
ansible.builtin.set_fact:
remote_eth0_mac: >-
{{
(
(dev2_eth0_mac_raw.stdout | default('') | regex_replace('\r','')) ~ "\n" ~
(dev2_eth0_mac_raw.stderr | default('') | regex_replace('\r',''))
)
| regex_findall('([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})')
| last
| default('')
| lower
}}
changed_when: false
- name: Abort if remote eth0 MAC != discovered DEV2 MAC
when: dev2_conn_method == "tunnel" and dev2_passfile_used_tunnel != "NONE" and (dev2_mac | default('') | trim | length > 0) and ((dev2_eth0_mac_read.stdout | default('') | trim | lower) != (dev2_mac | trim | lower))
when:
- dev2_conn_method == "tunnel"
- (dev2_passfile_used_tunnel | default('NONE')) != "NONE"
- (dev2_mac | default('') | length) > 0
- (remote_eth0_mac | default('') | length) > 0
- (remote_eth0_mac | lower) != (dev2_mac | lower)
ansible.builtin.fail:
msg: >
msg: >-
Safety stop: tunnel reached wrong device.
expected_dev2_mac={{ dev2_mac | trim }},
remote_eth0_mac={{ dev2_eth0_mac_read.stdout | default('') | trim }}.
expected_dev2_mac={{ dev2_mac }}, remote_eth0_mac={{ remote_eth0_mac }}
# ============================ FALLBACKS (only if tunnel/direct failed) ============================
# LLDP IPv4 fallback is only for 10.x in sot-updater; keep that behavior:
- name: Try DEV2 login via LLDP IPv4 10.x (fallback if tunnel auth failed)
when:
- dev2_conn_method == "tunnel"
@@ -409,7 +431,7 @@
for f in {{ dev2_passfiles | join(' ') }}; do
if timeout 20s sshpass -f "$f" ssh {{ ssh_opts_common }} \
-o AddressFamily=inet \
-o ConnectionAttempts=1 -o ConnectTimeout=10 \
-o ConnectTimeout=10 \
"{{ dev2_ssh_user }}@${HOST}" echo OK >/dev/null 2>&1; then
echo "$f"; exit 0
fi
@@ -431,7 +453,6 @@
dev2_passfile_used_lldp4: "{{ (dev2_auth_lldp4.rc == 0) | ternary((dev2_auth_lldp4.stdout | trim), 'NONE') }}"
changed_when: false
# LLDP IPv6 nested fallback (last resort): only if tunnel and lldp4 failed
- name: Copy DEV2 passfiles to DEV1 for IPv6 nested SSH (last resort)
when:
- dev2_conn_method == "tunnel"
@@ -457,10 +478,10 @@
IP6="{{ lldp_dev2_ip6 }}"
for f in {{ dev2_passfiles | join(' ') }}; do
if sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \
-o ConnectionAttempts=1 \
"{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \
"timeout {{ ssh_timeout }}s sshpass -f '/tmp/${f}' ssh \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
-o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout={{ ssh_timeout }} \
'{{ dev2_ssh_user }}@\[${IP6}%{{ dev1_iface }}\]' 'echo OK' " \
@@ -527,14 +548,13 @@
- "lldp ipv4={{ lldp_dev2_ip | default('') }}"
- "lldp ipv6={{ lldp_dev2_ip6 | default('') }}"
# ---- Wrapper: produce a single executable snippet in dev2_exec_cmd ----
# ---- Wrapper: produce a single executable snippet in dev2_exec_cmd (for Phase 1 commands later) ----
- name: Build DEV2 exec wrapper (controller-side) for Phase 1 commands
delegate_to: localhost
ansible.builtin.set_fact:
dev2_exec_cmd: |
set -e
MODE="{{ dev2_conn_final }}"
# DEV2_CMD is injected by Phase 1 tasks
if [ -z "${DEV2_CMD:-}" ]; then
echo "ERROR: DEV2_CMD is empty" >&2
exit 2
@@ -546,7 +566,6 @@
PASS="{{ dev2_passfile_used_tunnel }}"
sshpass -f "$PASS" ssh {{ ssh_opts_common }} \
-o AddressFamily=inet \
-o ConnectionAttempts=1 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"{{ pathprefix }} ${DEV2_CMD}" 2>&1
;;
@@ -555,7 +574,6 @@
PASS="{{ dev2_passfile_used_direct }}"
sshpass -f "$PASS" ssh {{ ssh_opts_common }} \
-o AddressFamily=inet \
-o ConnectionAttempts=1 \
"{{ dev2_ssh_user }}@${HOST}" \
"{{ pathprefix }} ${DEV2_CMD}" 2>&1
;;
@@ -564,7 +582,6 @@
PASS="{{ dev2_passfile_used_lldp4 }}"
sshpass -f "$PASS" ssh {{ ssh_opts_common }} \
-o AddressFamily=inet \
-o ConnectionAttempts=1 \
"{{ dev2_ssh_user }}@${HOST}" \
"{{ pathprefix }} ${DEV2_CMD}" 2>&1
;;
@@ -572,10 +589,10 @@
IP6="{{ lldp_dev2_ip6 }}"
F="{{ dev2_passfile_used_lldp6 }}"
sshpass -p "{{ dev1_pass }}" ssh {{ ssh_opts_common }} \
-o ConnectionAttempts=1 \
"{{ dev1_user }}@{{ ansible_host | default(inventory_hostname) }}" \
"sshpass -f '/tmp/${F}' ssh \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
-o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout={{ ssh_timeout }} \
'{{ dev2_ssh_user }}@\[${IP6}%{{ dev1_iface }}\]' \
@@ -588,31 +605,6 @@
esac
changed_when: false
- name: "PPPoE to super DHCP | Phase 1 | On-device actions (initially uptime)"
hosts: all
gather_facts: no
tasks:
# NOTE: Phase 1 assumes Phase 0 facts are still available in-play execution.
# If your runner executes plays independently, merge into one play.
- name: DEV2 | exec wrapper run (Phase 1)
delegate_to: localhost
ansible.builtin.shell: |
export DEV2_CMD="uptime"
{{ dev2_exec_cmd }}
args: { executable: /bin/bash }
register: dev2_exec
changed_when: false
failed_when: false
- name: DEV2 | uptime (stdout)
delegate_to: localhost
ansible.builtin.debug:
msg:
- "DEV2 uptime via {{ dev2_conn_final }}:"
- "{{ dev2_exec.stdout | default('') | regex_replace('\r','') }}"
post_tasks:
- name: Cleanup note
delegate_to: localhost