1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li"""This class extends pexpect.spawn to specialize setting up SSH connections. 3*9c5db199SXin LiThis adds methods for login, logout, and expecting the shell prompt. 4*9c5db199SXin Li 5*9c5db199SXin Li$Id: pxssh.py 487 2007-08-29 22:33:29Z noah $ 6*9c5db199SXin Li""" 7*9c5db199SXin Li 8*9c5db199SXin Lifrom __future__ import absolute_import 9*9c5db199SXin Lifrom __future__ import division 10*9c5db199SXin Lifrom __future__ import print_function 11*9c5db199SXin Li 12*9c5db199SXin Lifrom six.moves import range 13*9c5db199SXin Liimport time 14*9c5db199SXin Li 15*9c5db199SXin Lifrom autotest_lib.client.common_lib.pexpect import * 16*9c5db199SXin Lifrom autotest_lib.client.common_lib import pexpect 17*9c5db199SXin Li 18*9c5db199SXin Li 19*9c5db199SXin Li__all__ = ['ExceptionPxssh', 'pxssh'] 20*9c5db199SXin Li 21*9c5db199SXin Li# Exception classes used by this module. 22*9c5db199SXin Liclass ExceptionPxssh(ExceptionPexpect): 23*9c5db199SXin Li """Raised for pxssh exceptions. 24*9c5db199SXin Li """ 25*9c5db199SXin Li 26*9c5db199SXin Liclass pxssh (spawn): 27*9c5db199SXin Li 28*9c5db199SXin Li """This class extends pexpect.spawn to specialize setting up SSH 29*9c5db199SXin Li connections. This adds methods for login, logout, and expecting the shell 30*9c5db199SXin Li prompt. It does various tricky things to handle many situations in the SSH 31*9c5db199SXin Li login process. For example, if the session is your first login, then pxssh 32*9c5db199SXin Li automatically accepts the remote certificate; or if you have public key 33*9c5db199SXin Li authentication setup then pxssh won't wait for the password prompt. 34*9c5db199SXin Li 35*9c5db199SXin Li pxssh uses the shell prompt to synchronize output from the remote host. In 36*9c5db199SXin Li order to make this more robust it sets the shell prompt to something more 37*9c5db199SXin Li unique than just $ or #. This should work on most Borne/Bash or Csh style 38*9c5db199SXin Li shells. 39*9c5db199SXin Li 40*9c5db199SXin Li Example that runs a few commands on a remote server and prints the result:: 41*9c5db199SXin Li 42*9c5db199SXin Li import pxssh 43*9c5db199SXin Li import getpass 44*9c5db199SXin Li try: 45*9c5db199SXin Li s = pxssh.pxssh() 46*9c5db199SXin Li hostname = raw_input('hostname: ') 47*9c5db199SXin Li username = raw_input('username: ') 48*9c5db199SXin Li password = getpass.getpass('password: ') 49*9c5db199SXin Li s.login (hostname, username, password) 50*9c5db199SXin Li s.sendline ('uptime') # run a command 51*9c5db199SXin Li s.prompt() # match the prompt 52*9c5db199SXin Li print s.before # print everything before the prompt. 53*9c5db199SXin Li s.sendline ('ls -l') 54*9c5db199SXin Li s.prompt() 55*9c5db199SXin Li print s.before 56*9c5db199SXin Li s.sendline ('df') 57*9c5db199SXin Li s.prompt() 58*9c5db199SXin Li print s.before 59*9c5db199SXin Li s.logout() 60*9c5db199SXin Li except pxssh.ExceptionPxssh, e: 61*9c5db199SXin Li print "pxssh failed on login." 62*9c5db199SXin Li print str(e) 63*9c5db199SXin Li 64*9c5db199SXin Li Note that if you have ssh-agent running while doing development with pxssh 65*9c5db199SXin Li then this can lead to a lot of confusion. Many X display managers (xdm, 66*9c5db199SXin Li gdm, kdm, etc.) will automatically start a GUI agent. You may see a GUI 67*9c5db199SXin Li dialog box popup asking for a password during development. You should turn 68*9c5db199SXin Li off any key agents during testing. The 'force_password' attribute will turn 69*9c5db199SXin Li off public key authentication. This will only work if the remote SSH server 70*9c5db199SXin Li is configured to allow password logins. Example of using 'force_password' 71*9c5db199SXin Li attribute:: 72*9c5db199SXin Li 73*9c5db199SXin Li s = pxssh.pxssh() 74*9c5db199SXin Li s.force_password = True 75*9c5db199SXin Li hostname = raw_input('hostname: ') 76*9c5db199SXin Li username = raw_input('username: ') 77*9c5db199SXin Li password = getpass.getpass('password: ') 78*9c5db199SXin Li s.login (hostname, username, password) 79*9c5db199SXin Li """ 80*9c5db199SXin Li 81*9c5db199SXin Li def __init__ (self, timeout=30, maxread=2000, searchwindowsize=None, logfile=None, cwd=None, env=None): 82*9c5db199SXin Li spawn.__init__(self, None, timeout=timeout, maxread=maxread, searchwindowsize=searchwindowsize, logfile=logfile, cwd=cwd, env=env) 83*9c5db199SXin Li 84*9c5db199SXin Li self.name = '<pxssh>' 85*9c5db199SXin Li 86*9c5db199SXin Li #SUBTLE HACK ALERT! Note that the command to set the prompt uses a 87*9c5db199SXin Li #slightly different string than the regular expression to match it. This 88*9c5db199SXin Li #is because when you set the prompt the command will echo back, but we 89*9c5db199SXin Li #don't want to match the echoed command. So if we make the set command 90*9c5db199SXin Li #slightly different than the regex we eliminate the problem. To make the 91*9c5db199SXin Li #set command different we add a backslash in front of $. The $ doesn't 92*9c5db199SXin Li #need to be escaped, but it doesn't hurt and serves to make the set 93*9c5db199SXin Li #prompt command different than the regex. 94*9c5db199SXin Li 95*9c5db199SXin Li # used to match the command-line prompt 96*9c5db199SXin Li self.UNIQUE_PROMPT = "\[PEXPECT\][\$\#] " 97*9c5db199SXin Li self.PROMPT = self.UNIQUE_PROMPT 98*9c5db199SXin Li 99*9c5db199SXin Li # used to set shell command-line prompt to UNIQUE_PROMPT. 100*9c5db199SXin Li self.PROMPT_SET_SH = "PS1='[PEXPECT]\$ '" 101*9c5db199SXin Li self.PROMPT_SET_CSH = "set prompt='[PEXPECT]\$ '" 102*9c5db199SXin Li self.SSH_OPTS = "-o'RSAAuthentication=no' -o 'PubkeyAuthentication=no'" 103*9c5db199SXin Li # Disabling X11 forwarding gets rid of the annoying SSH_ASKPASS from 104*9c5db199SXin Li # displaying a GUI password dialog. I have not figured out how to 105*9c5db199SXin Li # disable only SSH_ASKPASS without also disabling X11 forwarding. 106*9c5db199SXin Li # Unsetting SSH_ASKPASS on the remote side doesn't disable it! Annoying! 107*9c5db199SXin Li #self.SSH_OPTS = "-x -o'RSAAuthentication=no' -o 'PubkeyAuthentication=no'" 108*9c5db199SXin Li self.force_password = False 109*9c5db199SXin Li self.auto_prompt_reset = True 110*9c5db199SXin Li 111*9c5db199SXin Li def levenshtein_distance(self, a,b): 112*9c5db199SXin Li 113*9c5db199SXin Li """This calculates the Levenshtein distance between a and b. 114*9c5db199SXin Li """ 115*9c5db199SXin Li 116*9c5db199SXin Li n, m = len(a), len(b) 117*9c5db199SXin Li if n > m: 118*9c5db199SXin Li a,b = b,a 119*9c5db199SXin Li n,m = m,n 120*9c5db199SXin Li current = list(range(n+1)) 121*9c5db199SXin Li for i in range(1,m+1): 122*9c5db199SXin Li previous, current = current, [i]+[0]*n 123*9c5db199SXin Li for j in range(1,n+1): 124*9c5db199SXin Li add, delete = previous[j]+1, current[j-1]+1 125*9c5db199SXin Li change = previous[j-1] 126*9c5db199SXin Li if a[j-1] != b[i-1]: 127*9c5db199SXin Li change = change + 1 128*9c5db199SXin Li current[j] = min(add, delete, change) 129*9c5db199SXin Li return current[n] 130*9c5db199SXin Li 131*9c5db199SXin Li def synch_original_prompt (self): 132*9c5db199SXin Li 133*9c5db199SXin Li """This attempts to find the prompt. Basically, press enter and record 134*9c5db199SXin Li the response; press enter again and record the response; if the two 135*9c5db199SXin Li responses are similar then assume we are at the original prompt. """ 136*9c5db199SXin Li 137*9c5db199SXin Li # All of these timing pace values are magic. 138*9c5db199SXin Li # I came up with these based on what seemed reliable for 139*9c5db199SXin Li # connecting to a heavily loaded machine I have. 140*9c5db199SXin Li # If latency is worse than these values then this will fail. 141*9c5db199SXin Li 142*9c5db199SXin Li self.sendline() 143*9c5db199SXin Li time.sleep(0.5) 144*9c5db199SXin Li self.read_nonblocking(size=10000,timeout=1) # GAS: Clear out the cache before getting the prompt 145*9c5db199SXin Li time.sleep(0.1) 146*9c5db199SXin Li self.sendline() 147*9c5db199SXin Li time.sleep(0.5) 148*9c5db199SXin Li x = self.read_nonblocking(size=1000,timeout=1) 149*9c5db199SXin Li time.sleep(0.1) 150*9c5db199SXin Li self.sendline() 151*9c5db199SXin Li time.sleep(0.5) 152*9c5db199SXin Li a = self.read_nonblocking(size=1000,timeout=1) 153*9c5db199SXin Li time.sleep(0.1) 154*9c5db199SXin Li self.sendline() 155*9c5db199SXin Li time.sleep(0.5) 156*9c5db199SXin Li b = self.read_nonblocking(size=1000,timeout=1) 157*9c5db199SXin Li ld = self.levenshtein_distance(a,b) 158*9c5db199SXin Li len_a = len(a) 159*9c5db199SXin Li if len_a == 0: 160*9c5db199SXin Li return False 161*9c5db199SXin Li if float(ld)/len_a < 0.4: 162*9c5db199SXin Li return True 163*9c5db199SXin Li return False 164*9c5db199SXin Li 165*9c5db199SXin Li ### TODO: This is getting messy and I'm pretty sure this isn't perfect. 166*9c5db199SXin Li ### TODO: I need to draw a flow chart for this. 167*9c5db199SXin Li def login (self,server,username,password='',terminal_type='ansi',original_prompt=r"[#$]",login_timeout=10,port=None,auto_prompt_reset=True): 168*9c5db199SXin Li 169*9c5db199SXin Li """This logs the user into the given server. It uses the 170*9c5db199SXin Li 'original_prompt' to try to find the prompt right after login. When it 171*9c5db199SXin Li finds the prompt it immediately tries to reset the prompt to something 172*9c5db199SXin Li more easily matched. The default 'original_prompt' is very optimistic 173*9c5db199SXin Li and is easily fooled. It's more reliable to try to match the original 174*9c5db199SXin Li prompt as exactly as possible to prevent false matches by server 175*9c5db199SXin Li strings such as the "Message Of The Day". On many systems you can 176*9c5db199SXin Li disable the MOTD on the remote server by creating a zero-length file 177*9c5db199SXin Li called "~/.hushlogin" on the remote server. If a prompt cannot be found 178*9c5db199SXin Li then this will not necessarily cause the login to fail. In the case of 179*9c5db199SXin Li a timeout when looking for the prompt we assume that the original 180*9c5db199SXin Li prompt was so weird that we could not match it, so we use a few tricks 181*9c5db199SXin Li to guess when we have reached the prompt. Then we hope for the best and 182*9c5db199SXin Li blindly try to reset the prompt to something more unique. If that fails 183*9c5db199SXin Li then login() raises an ExceptionPxssh exception. 184*9c5db199SXin Li 185*9c5db199SXin Li In some situations it is not possible or desirable to reset the 186*9c5db199SXin Li original prompt. In this case, set 'auto_prompt_reset' to False to 187*9c5db199SXin Li inhibit setting the prompt to the UNIQUE_PROMPT. Remember that pxssh 188*9c5db199SXin Li uses a unique prompt in the prompt() method. If the original prompt is 189*9c5db199SXin Li not reset then this will disable the prompt() method unless you 190*9c5db199SXin Li manually set the PROMPT attribute. """ 191*9c5db199SXin Li 192*9c5db199SXin Li ssh_options = '-q' 193*9c5db199SXin Li if self.force_password: 194*9c5db199SXin Li ssh_options = ssh_options + ' ' + self.SSH_OPTS 195*9c5db199SXin Li if port is not None: 196*9c5db199SXin Li ssh_options = ssh_options + ' -p %s'%(str(port)) 197*9c5db199SXin Li cmd = "ssh %s -l %s %s" % (ssh_options, username, server) 198*9c5db199SXin Li 199*9c5db199SXin Li # This does not distinguish between a remote server 'password' prompt 200*9c5db199SXin Li # and a local ssh 'passphrase' prompt (for unlocking a private key). 201*9c5db199SXin Li spawn._spawn(self, cmd) 202*9c5db199SXin Li i = self.expect(["(?i)are you sure you want to continue connecting", original_prompt, "(?i)(?:password)|(?:passphrase for key)", "(?i)permission denied", "(?i)terminal type", TIMEOUT, "(?i)connection closed by remote host"], timeout=login_timeout) 203*9c5db199SXin Li 204*9c5db199SXin Li # First phase 205*9c5db199SXin Li if i==0: 206*9c5db199SXin Li # New certificate -- always accept it. 207*9c5db199SXin Li # This is what you get if SSH does not have the remote host's 208*9c5db199SXin Li # public key stored in the 'known_hosts' cache. 209*9c5db199SXin Li self.sendline("yes") 210*9c5db199SXin Li i = self.expect(["(?i)are you sure you want to continue connecting", original_prompt, "(?i)(?:password)|(?:passphrase for key)", "(?i)permission denied", "(?i)terminal type", TIMEOUT]) 211*9c5db199SXin Li if i==2: # password or passphrase 212*9c5db199SXin Li self.sendline(password) 213*9c5db199SXin Li i = self.expect(["(?i)are you sure you want to continue connecting", original_prompt, "(?i)(?:password)|(?:passphrase for key)", "(?i)permission denied", "(?i)terminal type", TIMEOUT]) 214*9c5db199SXin Li if i==4: 215*9c5db199SXin Li self.sendline(terminal_type) 216*9c5db199SXin Li i = self.expect(["(?i)are you sure you want to continue connecting", original_prompt, "(?i)(?:password)|(?:passphrase for key)", "(?i)permission denied", "(?i)terminal type", TIMEOUT]) 217*9c5db199SXin Li 218*9c5db199SXin Li # Second phase 219*9c5db199SXin Li if i==0: 220*9c5db199SXin Li # This is weird. This should not happen twice in a row. 221*9c5db199SXin Li self.close() 222*9c5db199SXin Li raise ExceptionPxssh ('Weird error. Got "are you sure" prompt twice.') 223*9c5db199SXin Li elif i==1: # can occur if you have a public key pair set to authenticate. 224*9c5db199SXin Li ### TODO: May NOT be OK if expect() got tricked and matched a false prompt. 225*9c5db199SXin Li pass 226*9c5db199SXin Li elif i==2: # password prompt again 227*9c5db199SXin Li # For incorrect passwords, some ssh servers will 228*9c5db199SXin Li # ask for the password again, others return 'denied' right away. 229*9c5db199SXin Li # If we get the password prompt again then this means 230*9c5db199SXin Li # we didn't get the password right the first time. 231*9c5db199SXin Li self.close() 232*9c5db199SXin Li raise ExceptionPxssh ('password refused') 233*9c5db199SXin Li elif i==3: # permission denied -- password was bad. 234*9c5db199SXin Li self.close() 235*9c5db199SXin Li raise ExceptionPxssh ('permission denied') 236*9c5db199SXin Li elif i == 4: # terminal type again? 237*9c5db199SXin Li self.close() 238*9c5db199SXin Li raise ExceptionPxssh ('Weird error. Got "terminal type" prompt twice.') 239*9c5db199SXin Li elif i==5: # Timeout 240*9c5db199SXin Li #This is tricky... I presume that we are at the command-line prompt. 241*9c5db199SXin Li #It may be that the shell prompt was so weird that we couldn't match 242*9c5db199SXin Li #it. Or it may be that we couldn't log in for some other reason. I 243*9c5db199SXin Li #can't be sure, but it's safe to guess that we did login because if 244*9c5db199SXin Li #I presume wrong and we are not logged in then this should be caught 245*9c5db199SXin Li #later when I try to set the shell prompt. 246*9c5db199SXin Li pass 247*9c5db199SXin Li elif i==6: # Connection closed by remote host 248*9c5db199SXin Li self.close() 249*9c5db199SXin Li raise ExceptionPxssh ('connection closed') 250*9c5db199SXin Li else: # Unexpected 251*9c5db199SXin Li self.close() 252*9c5db199SXin Li raise ExceptionPxssh ('unexpected login response') 253*9c5db199SXin Li if not self.synch_original_prompt(): 254*9c5db199SXin Li self.close() 255*9c5db199SXin Li raise ExceptionPxssh ('could not synchronize with original prompt') 256*9c5db199SXin Li # We appear to be in. 257*9c5db199SXin Li # set shell prompt to something unique. 258*9c5db199SXin Li if auto_prompt_reset: 259*9c5db199SXin Li if not self.set_unique_prompt(): 260*9c5db199SXin Li self.close() 261*9c5db199SXin Li raise ExceptionPxssh ('could not set shell prompt\n'+self.before) 262*9c5db199SXin Li return True 263*9c5db199SXin Li 264*9c5db199SXin Li def logout (self): 265*9c5db199SXin Li 266*9c5db199SXin Li """This sends exit to the remote shell. If there are stopped jobs then 267*9c5db199SXin Li this automatically sends exit twice. """ 268*9c5db199SXin Li 269*9c5db199SXin Li self.sendline("exit") 270*9c5db199SXin Li index = self.expect([EOF, "(?i)there are stopped jobs"]) 271*9c5db199SXin Li if index==1: 272*9c5db199SXin Li self.sendline("exit") 273*9c5db199SXin Li self.expect(EOF) 274*9c5db199SXin Li self.close() 275*9c5db199SXin Li 276*9c5db199SXin Li def prompt (self, timeout=20): 277*9c5db199SXin Li 278*9c5db199SXin Li """This matches the shell prompt. This is little more than a short-cut 279*9c5db199SXin Li to the expect() method. This returns True if the shell prompt was 280*9c5db199SXin Li matched. This returns False if there was a timeout. Note that if you 281*9c5db199SXin Li called login() with auto_prompt_reset set to False then you should have 282*9c5db199SXin Li manually set the PROMPT attribute to a regex pattern for matching the 283*9c5db199SXin Li prompt. """ 284*9c5db199SXin Li 285*9c5db199SXin Li i = self.expect([self.PROMPT, TIMEOUT], timeout=timeout) 286*9c5db199SXin Li if i==1: 287*9c5db199SXin Li return False 288*9c5db199SXin Li return True 289*9c5db199SXin Li 290*9c5db199SXin Li def set_unique_prompt (self): 291*9c5db199SXin Li 292*9c5db199SXin Li """This sets the remote prompt to something more unique than # or $. 293*9c5db199SXin Li This makes it easier for the prompt() method to match the shell prompt 294*9c5db199SXin Li unambiguously. This method is called automatically by the login() 295*9c5db199SXin Li method, but you may want to call it manually if you somehow reset the 296*9c5db199SXin Li shell prompt. For example, if you 'su' to a different user then you 297*9c5db199SXin Li will need to manually reset the prompt. This sends shell commands to 298*9c5db199SXin Li the remote host to set the prompt, so this assumes the remote host is 299*9c5db199SXin Li ready to receive commands. 300*9c5db199SXin Li 301*9c5db199SXin Li Alternatively, you may use your own prompt pattern. Just set the PROMPT 302*9c5db199SXin Li attribute to a regular expression that matches it. In this case you 303*9c5db199SXin Li should call login() with auto_prompt_reset=False; then set the PROMPT 304*9c5db199SXin Li attribute. After that the prompt() method will try to match your prompt 305*9c5db199SXin Li pattern.""" 306*9c5db199SXin Li 307*9c5db199SXin Li self.sendline ("unset PROMPT_COMMAND") 308*9c5db199SXin Li self.sendline (self.PROMPT_SET_SH) # sh-style 309*9c5db199SXin Li i = self.expect ([TIMEOUT, self.PROMPT], timeout=10) 310*9c5db199SXin Li if i == 0: # csh-style 311*9c5db199SXin Li self.sendline (self.PROMPT_SET_CSH) 312*9c5db199SXin Li i = self.expect ([TIMEOUT, self.PROMPT], timeout=10) 313*9c5db199SXin Li if i == 0: 314*9c5db199SXin Li return False 315*9c5db199SXin Li return True 316*9c5db199SXin Li 317*9c5db199SXin Li# vi:ts=4:sw=4:expandtab:ft=python: 318