Hi there. I've been forced by circumstance to write an expect script to handle password updates on a number of servers. There's a mix of Solaris 8, 9, 10, RedHat and Ubuntu. There's no chance the client will allow us to hook them up to a directory, so we have to make do.
This script is mostly intended for myself and my colleagues to update our own passwords, but I've built in the ability for it to update a user's passwords. I've also elected to use ssh $server sudo passwd to get around the not-standardised password policies. It has a wrapper script written in bash, so in the near future I may enforce a password policy there.
I've found some unexpected behaviour though. As part of error-handling, I'm trying to cater for instances where the script logs in to a server as the sysadmin, but finds that their password is expired. Here's the relevant bit of the script, and hopefully the comments will help you follow the logic:
# Open the connection and invoke sudo passwd
spawn ssh -q -o StrictHostKeyChecking=no -t $server $sudo passwd $user
expect {
# Set the default action to catch any unexpected responses
default {
send_user -- "\nERROR: I was unable to connect to $server for some reason. Please try manually.\n"
exit 1
}
# Pre-check for a server that we might not have an ssh key on yet
-re "$whoami@$server's \[pP\]assword:" {
send_log -- "\nWARN: $whoami doesn't appear to have an sshkey setup for $server. You might want to resolve that.\n"
send "$adminpass\r"
exp_continue
}
###### Note: the below block doesn't really work that well. The if statement is fine, but the sending of the adminpass is echo'd back, which messes the following expect
# Firstly, cater for password expiry for the admin account
-re "\\\(current\\\) UNIX \[pP\]assword:|\[oO\]ld \[pP\]assword:" {
# We need to test if the $user variable is the same as $whoami
# This is to cater for instances where the admin is updating his/her own password
# If true, it's sensible to feed the adminpass as their oldpwd, and newpass as their newpwd
# Otherwise if they're updating a user, we don't want to set the admin's newpwd to the user's newpwd!
if { $whoami != $user } {
send_user -- "\nWARN: It looks like you're updating a user's password\nI don't want to set your password to the user's password.\n"
send_log -- "\nWARN: $whoami's account is expired on $server and needs to be manually sorted out.\n"
# Try to end the remote session with Ctrl-D
send \004
exit 1
} else {
# If the above test passes, we assume the Admin is updating his/her own password and continue
send "$adminpass\r"
expect {
default {
send_user -- "\nERROR: It appears we can't authenticate to $server. You'll need to resolve that manually.\n"
exit 1
}
-re "\[nN\]ew UNIX \[pP\]assword:|\[nN\]ew \[pP\]assword:|\[aA\]gain:|\[rR\]etype|\[rR\]e-enter|\[rR\]eenter" {
# Imitate inferior, slow, meatbag humans. Mitigates mismatches.
sleep 2
send "$newpass\r"
# exp_continue loops us back to the expect, capturing the second New Password prompt
exp_continue
}
"BAD PASSWORD" {
send_user -- "\nWARN: Password change failed for $whoami on $server due to the password not being acceptable. Try a more complex password.\n"
exit 1
}
# Upon completion of the password change, passwd should tell us it was successful. We use this to exit
# This prevents erroneous triggering of the default condition
-re "updated|successfully" {
send_log -- "\nINFO: Password updated on $server for $whoami.\n"
exit 0
}
}
}
}
Here's the behaviour (sanitised and using QWERTYuiop12345 as an example password)
$ expect expect.chpwd examplehost me
spawn ssh -q -o StrictHostKeyChecking=no -t examplehost sudo passwd me
me@examplehost's password:
WARNING: Your password has expired.
You must change your password now and login again!
Changing password for user me.
Changing password for me
(current) UNIX password:
QWERTYuiop12345
passwd: Authentication token manipulation error
ERROR: It appears we can't authenticate to examplehost. You'll need to resolve that manually.
And here's the debug:
expect: does "" (spawn_id exp7) match regular expression "me@examplehost's [pP]assword:"? Gate "me@examplehost's ?assword:"? gate=no
"\(current\) UNIX [pP]assword:|[oO]ld [pP]assword:"? (No Gate, RE only) gate=yes re=no
"\[sudo\] password for|[pP]assword:"? (No Gate, RE only) gate=yes re=no
me@examplehost's password:
expect: does "me@examplehost's password: " (spawn_id exp7) match regular expression "me@examplehost's [pP]assword:"? Gate "me@examplehost's ?assword:"? gate=yes re=yes
expect: set expect_out(0,string) "me@examplehost's password:"
expect: set expect_out(spawn_id) "exp7"
expect: set expect_out(buffer) "me@examplehost's password:"
send: sending "QWERTYuiop12345\n\r" to { exp7 }
expect: continuing expect
expect: does " " (spawn_id exp7) match regular expression "me@examplehost's [pP]assword:"? Gate "me@examplehost's ?assword:"? gate=no
"\(current\) UNIX [pP]assword:|[oO]ld [pP]assword:"? (No Gate, RE only) gate=yes re=no
"\[sudo\] password for|[pP]assword:"? (No Gate, RE only) gate=yes re=no
expect: does " \r\n" (spawn_id exp7) match regular expression "me@examplehost's [pP]assword:"? Gate "me@examplehost's ?assword:"? gate=no
"\(current\) UNIX [pP]assword:|[oO]ld [pP]assword:"? (No Gate, RE only) gate=yes re=no
"\[sudo\] password for|[pP]assword:"? (No Gate, RE only) gate=yes re=no
WARNING: Your password has expired.
You must change your password now and login again!
expect: does " \r\nWARNING: Your password has expired.\r\nYou must change your password now and login again!\r\n" (spawn_id exp7)
match regular expression "me@examplehost's [pP]assword:"? Gate "me@examplehost's ?assword:"? gate=no
"\(current\) UNIX [pP]assword:|[oO]ld [pP]assword:"? (No Gate, RE only) gate=yes re=no
"\[sudo\] password for|[pP]assword:"? (No Gate, RE only) gate=yes re=no
Changing password for user me.
Changing password for me
(current) UNIX password:
expect: does " \r\nWARNING: Your password has expired.\r\nYou must change your password now and login again!\r\nChanging password for user me.\r\nChanging password for me\r\n(current) UNIX password: \r\n" (spawn_id exp7)
match regular expression "me@examplehost's [pP]assword:"? Gate "me@examplehost's ?assword:"? gate=no
"\(current\) UNIX [pP]assword:|[oO]ld [pP]assword:"? (No Gate, RE only) gate=yes re=yes
expect: set expect_out(0,string) "(current) UNIX password:"
expect: set expect_out(spawn_id) "exp7"
expect: set expect_out(buffer) " \r\nWARNING: Your password has expired.\r\nYou must change your password now and login again!\r\nChanging password for user me.\r\nChanging password for me\r\n(current) UNIX password:"
send: sending "QWERTYuiop12345\n\r" to { exp7 }
Gate keeper glob pattern for '[nN]ew UNIX [pP]assword:|[nN]ew [pP]assword:|[aA]gain:|[rR]etype|[rR]e-enter|[rR]eenter' is ''. Not usable, disabling the performance booster.
Gate keeper glob pattern for 'updated|successfully' is ''. Not usable, disabling the performance booster.
expect: does " \r\n" (spawn_id exp7) match regular expression "[nN]ew UNIX [pP]assword:|[nN]ew [pP]assword:|[aA]gain:|[rR]etype|[rR]e-enter|[rR]eenter"? (No Gate, RE only) gate=yes re=no
"BAD PASSWORD"? no
"updated|successfully"? (No Gate, RE only) gate=yes re=no
QWERTYuiop12345
expect: does " \r\nQWERTYuiop12345\r\n\r\n" (spawn_id exp7) match regular expression "[nN]ew UNIX [pP]assword:|[nN]ew [pP]assword:|[aA]gain:|[rR]etype|[rR]e-enter|[rR]eenter"? (No Gate, RE only) gate=yes re=no
"BAD PASSWORD"? no
"updated|successfully"? (No Gate, RE only) gate=yes re=no
passwd: Authentication token manipulation error
expect: does " \r\nQWERTYuiop12345\r\n\r\npasswd: Authentication token manipulation error\r\r\n" (spawn_id exp7)
match regular expression "[nN]ew UNIX [pP]assword:|[nN]ew [pP]assword:|[aA]gain:|[rR]etype|[rR]e-enter|[rR]eenter"? (No Gate, RE only) gate=yes re=no
"BAD PASSWORD"? no
"updated|successfully"? (No Gate, RE only) gate=yes re=no
expect: read eof
expect: set expect_out(spawn_id) "exp7"
expect: set expect_out(buffer) " \r\nQWERTYuiop12345\r\n\r\npasswd: Authentication token manipulation error\r\r\n"
ERROR: It appears we can't authenticate to examplehost. You'll need to resolve that manually.
So you can see that -re "$whoami@$server's \[pP\]assword:" is found, the password is sent, and the script moves on. It then finds (current) UNIX password: and dutifully sends the password, however that password is printed out to the console (echoing off the remote ssh process?) and we get an Authentication token manipulation error.
The if/else works fine. I've also commented it entirely out and just had
-re "\\\(current\\\) UNIX \[pP\]assword:|\[oO\]ld \[pP\]assword:" {
send "$adminpass\r"
exp_continue
}
with the same behaviour. Could it be the buffer?
Apart from this one niggle, the rest of the script works exactly as desired.
Any thoughts appreciated.