#!/usr/bin/expect -f # RENDERED by Ansible (role: tplink_switch_harden). Do not edit the rendered copy. # Drives a TP-Link JetStream switch CLI over SSH. Password comes from $env(SWITCH_PW) # so it never appears in this file, the inventory, or the process arguments. set timeout {{ switch_expect_timeout | default(30) }} set host "{{ switch_mgmt_ip }}" set user "{{ switch_admin_user }}" set pw $env(SWITCH_PW) set apply {{ '1' if switch_apply | bool else '0' }} log_user 1 # Device prompt: User EXEC '>' or Privileged '#', optionally prefixed by hostname. set PROMPT {[\w./-]*[>#] ?$} proc wait_prompt {} { global PROMPT expect { -re {(?i)--more--|press any key|\(q to quit\)} { send -- " "; exp_continue } -re $PROMPT {} timeout { puts "\n>>>TIMEOUT-AT-PROMPT<<<"; exit 21 } eof { puts "\n>>>EOF-AT-PROMPT<<<"; exit 22 } } } proc do {cmd} { send -- "$cmd\r" wait_prompt } # NOTE: PubkeyAuthentication=no + PreferredAuthentications=password is REQUIRED. # The JetStream SSH server drops the connection when the client offers publickey # auth first (the default), which made logins appear to "close after KEX" at # random. Forcing password-only auth makes login deterministic. spawn ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ -o ConnectTimeout=15 -o NumberOfPasswordPrompts=1 \ -o PreferredAuthentications=password -o PubkeyAuthentication=no $user@$host expect { -re {(?i)password:} { log_user 0; send -- "$pw\r"; log_user 1; exp_continue } -re {(?i)(change.*password|new password|must.*change|set.*new password)} { puts "\n>>>FORCED-PASSWORD-CHANGE<<< switch still at first-login; aborting."; exit 9 } -re {(?i)(permission denied|authentication fail)} { puts "\n>>>AUTH-FAILED<<<"; exit 8 } -re $PROMPT {} timeout { puts "\n>>>LOGIN-TIMEOUT<<<"; exit 2 } eof { puts "\n>>>LOGIN-EOF<<<"; exit 3 } } # Enter privileged EXEC (default enable password is blank). send -- "enable\r" expect { -re {(?i)password:} { send -- "\r"; wait_prompt } -re $PROMPT {} timeout {} } # Always capture a read-only snapshot — proves access, changes nothing. puts "\n>>>===SHOW-START===<<<" do "show system-info" puts "\n>>>===SHOW-END===<<<" if {$apply == 1} { puts "\n>>>===APPLY-START===<<<" do "configure" {% for c in switch_harden_commands %} do {{ "{" ~ c.cmd ~ "}" }} ;# {{ c.desc }} {% endfor %} {% for c in switch_identity_commands %} do {{ "{" ~ c.cmd ~ "}" }} ;# {{ c.desc }} {% endfor %} {% if switch_apply_network | bool %} # NETWORK re-addressing. In steady state (already at the static IP) these are # no-ops. During the one-time DHCP->static cutover the `ip address` line drops # the session — do that via the dedicated migration step, not this engine. {% for c in switch_network_commands %} do {{ "{" ~ c.cmd ~ "}" }} ;# {{ c.desc }} {% endfor %} {% endif %} do "end" # Persist running-config to startup (JetStream may ask Y/N). send -- "copy running-config startup-config\r" expect { -re {(?i)(y/n|are you sure|\[y/n\]|continue)} { send -- "Y\r"; wait_prompt } -re $PROMPT {} timeout {} } puts "\n>>>===APPLY-END===<<<" } send -- "exit\r" catch {expect eof} puts "\n>>>===DONE===<<<"