From 1d269d343f6a023ad9575c1f2260723e48380aa5 Mon Sep 17 00:00:00 2001 From: pavel Date: Tue, 20 Jan 2026 10:11:00 +0200 Subject: [PATCH] 1010 --- files/ansible-playbooks/ptsd-migrate.yml | 106 +++++++++++------------ 1 file changed, 49 insertions(+), 57 deletions(-) diff --git a/files/ansible-playbooks/ptsd-migrate.yml b/files/ansible-playbooks/ptsd-migrate.yml index 61b5a60..267172d 100644 --- a/files/ansible-playbooks/ptsd-migrate.yml +++ b/files/ansible-playbooks/ptsd-migrate.yml @@ -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('') }}" # ============================ 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