1*061da546Spatrick'''This class extends pexpect.spawn to specialize setting up SSH connections. 2*061da546SpatrickThis adds methods for login, logout, and expecting the shell prompt. 3*061da546Spatrick 4*061da546SpatrickPEXPECT LICENSE 5*061da546Spatrick 6*061da546Spatrick This license is approved by the OSI and FSF as GPL-compatible. 7*061da546Spatrick http://opensource.org/licenses/isc-license.txt 8*061da546Spatrick 9*061da546Spatrick Copyright (c) 2012, Noah Spurrier <noah@noah.org> 10*061da546Spatrick PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY 11*061da546Spatrick PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE 12*061da546Spatrick COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. 13*061da546Spatrick THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 14*061da546Spatrick WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 15*061da546Spatrick MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 16*061da546Spatrick ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 17*061da546Spatrick WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 18*061da546Spatrick ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 19*061da546Spatrick OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 20*061da546Spatrick 21*061da546Spatrick''' 22*061da546Spatrick 23*061da546Spatrickfrom pexpect import ExceptionPexpect, TIMEOUT, EOF, spawn 24*061da546Spatrickimport time 25*061da546Spatrickimport os 26*061da546Spatrickimport sys 27*061da546Spatrickimport re 28*061da546Spatrick 29*061da546Spatrick__all__ = ['ExceptionPxssh', 'pxssh'] 30*061da546Spatrick 31*061da546Spatrick# Exception classes used by this module. 32*061da546Spatrickclass ExceptionPxssh(ExceptionPexpect): 33*061da546Spatrick '''Raised for pxssh exceptions. 34*061da546Spatrick ''' 35*061da546Spatrick 36*061da546Spatrickif sys.version_info > (3, 0): 37*061da546Spatrick from shlex import quote 38*061da546Spatrickelse: 39*061da546Spatrick _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search 40*061da546Spatrick 41*061da546Spatrick def quote(s): 42*061da546Spatrick """Return a shell-escaped version of the string *s*.""" 43*061da546Spatrick if not s: 44*061da546Spatrick return "''" 45*061da546Spatrick if _find_unsafe(s) is None: 46*061da546Spatrick return s 47*061da546Spatrick 48*061da546Spatrick # use single quotes, and put single quotes into double quotes 49*061da546Spatrick # the string $'b is then quoted as '$'"'"'b' 50*061da546Spatrick return "'" + s.replace("'", "'\"'\"'") + "'" 51*061da546Spatrick 52*061da546Spatrickclass pxssh (spawn): 53*061da546Spatrick '''This class extends pexpect.spawn to specialize setting up SSH 54*061da546Spatrick connections. This adds methods for login, logout, and expecting the shell 55*061da546Spatrick prompt. It does various tricky things to handle many situations in the SSH 56*061da546Spatrick login process. For example, if the session is your first login, then pxssh 57*061da546Spatrick automatically accepts the remote certificate; or if you have public key 58*061da546Spatrick authentication setup then pxssh won't wait for the password prompt. 59*061da546Spatrick 60*061da546Spatrick pxssh uses the shell prompt to synchronize output from the remote host. In 61*061da546Spatrick order to make this more robust it sets the shell prompt to something more 62*061da546Spatrick unique than just $ or #. This should work on most Borne/Bash or Csh style 63*061da546Spatrick shells. 64*061da546Spatrick 65*061da546Spatrick Example that runs a few commands on a remote server and prints the result:: 66*061da546Spatrick 67*061da546Spatrick from pexpect import pxssh 68*061da546Spatrick import getpass 69*061da546Spatrick try: 70*061da546Spatrick s = pxssh.pxssh() 71*061da546Spatrick hostname = raw_input('hostname: ') 72*061da546Spatrick username = raw_input('username: ') 73*061da546Spatrick password = getpass.getpass('password: ') 74*061da546Spatrick s.login(hostname, username, password) 75*061da546Spatrick s.sendline('uptime') # run a command 76*061da546Spatrick s.prompt() # match the prompt 77*061da546Spatrick print(s.before) # print everything before the prompt. 78*061da546Spatrick s.sendline('ls -l') 79*061da546Spatrick s.prompt() 80*061da546Spatrick print(s.before) 81*061da546Spatrick s.sendline('df') 82*061da546Spatrick s.prompt() 83*061da546Spatrick print(s.before) 84*061da546Spatrick s.logout() 85*061da546Spatrick except pxssh.ExceptionPxssh as e: 86*061da546Spatrick print("pxssh failed on login.") 87*061da546Spatrick print(e) 88*061da546Spatrick 89*061da546Spatrick Example showing how to specify SSH options:: 90*061da546Spatrick 91*061da546Spatrick from pexpect import pxssh 92*061da546Spatrick s = pxssh.pxssh(options={ 93*061da546Spatrick "StrictHostKeyChecking": "no", 94*061da546Spatrick "UserKnownHostsFile": "/dev/null"}) 95*061da546Spatrick ... 96*061da546Spatrick 97*061da546Spatrick Note that if you have ssh-agent running while doing development with pxssh 98*061da546Spatrick then this can lead to a lot of confusion. Many X display managers (xdm, 99*061da546Spatrick gdm, kdm, etc.) will automatically start a GUI agent. You may see a GUI 100*061da546Spatrick dialog box popup asking for a password during development. You should turn 101*061da546Spatrick off any key agents during testing. The 'force_password' attribute will turn 102*061da546Spatrick off public key authentication. This will only work if the remote SSH server 103*061da546Spatrick is configured to allow password logins. Example of using 'force_password' 104*061da546Spatrick attribute:: 105*061da546Spatrick 106*061da546Spatrick s = pxssh.pxssh() 107*061da546Spatrick s.force_password = True 108*061da546Spatrick hostname = raw_input('hostname: ') 109*061da546Spatrick username = raw_input('username: ') 110*061da546Spatrick password = getpass.getpass('password: ') 111*061da546Spatrick s.login (hostname, username, password) 112*061da546Spatrick 113*061da546Spatrick `debug_command_string` is only for the test suite to confirm that the string 114*061da546Spatrick generated for SSH is correct, using this will not allow you to do 115*061da546Spatrick anything other than get a string back from `pxssh.pxssh.login()`. 116*061da546Spatrick ''' 117*061da546Spatrick 118*061da546Spatrick def __init__ (self, timeout=30, maxread=2000, searchwindowsize=None, 119*061da546Spatrick logfile=None, cwd=None, env=None, ignore_sighup=True, echo=True, 120*061da546Spatrick options={}, encoding=None, codec_errors='strict', 121*061da546Spatrick debug_command_string=False): 122*061da546Spatrick 123*061da546Spatrick spawn.__init__(self, None, timeout=timeout, maxread=maxread, 124*061da546Spatrick searchwindowsize=searchwindowsize, logfile=logfile, 125*061da546Spatrick cwd=cwd, env=env, ignore_sighup=ignore_sighup, echo=echo, 126*061da546Spatrick encoding=encoding, codec_errors=codec_errors) 127*061da546Spatrick 128*061da546Spatrick self.name = '<pxssh>' 129*061da546Spatrick 130*061da546Spatrick #SUBTLE HACK ALERT! Note that the command that SETS the prompt uses a 131*061da546Spatrick #slightly different string than the regular expression to match it. This 132*061da546Spatrick #is because when you set the prompt the command will echo back, but we 133*061da546Spatrick #don't want to match the echoed command. So if we make the set command 134*061da546Spatrick #slightly different than the regex we eliminate the problem. To make the 135*061da546Spatrick #set command different we add a backslash in front of $. The $ doesn't 136*061da546Spatrick #need to be escaped, but it doesn't hurt and serves to make the set 137*061da546Spatrick #prompt command different than the regex. 138*061da546Spatrick 139*061da546Spatrick # used to match the command-line prompt 140*061da546Spatrick self.UNIQUE_PROMPT = r"\[PEXPECT\][\$\#] " 141*061da546Spatrick self.PROMPT = self.UNIQUE_PROMPT 142*061da546Spatrick 143*061da546Spatrick # used to set shell command-line prompt to UNIQUE_PROMPT. 144*061da546Spatrick self.PROMPT_SET_SH = r"PS1='[PEXPECT]\$ '" 145*061da546Spatrick self.PROMPT_SET_CSH = r"set prompt='[PEXPECT]\$ '" 146*061da546Spatrick self.SSH_OPTS = ("-o'RSAAuthentication=no'" 147*061da546Spatrick + " -o 'PubkeyAuthentication=no'") 148*061da546Spatrick# Disabling host key checking, makes you vulnerable to MITM attacks. 149*061da546Spatrick# + " -o 'StrictHostKeyChecking=no'" 150*061da546Spatrick# + " -o 'UserKnownHostsFile /dev/null' ") 151*061da546Spatrick # Disabling X11 forwarding gets rid of the annoying SSH_ASKPASS from 152*061da546Spatrick # displaying a GUI password dialog. I have not figured out how to 153*061da546Spatrick # disable only SSH_ASKPASS without also disabling X11 forwarding. 154*061da546Spatrick # Unsetting SSH_ASKPASS on the remote side doesn't disable it! Annoying! 155*061da546Spatrick #self.SSH_OPTS = "-x -o'RSAAuthentication=no' -o 'PubkeyAuthentication=no'" 156*061da546Spatrick self.force_password = False 157*061da546Spatrick 158*061da546Spatrick self.debug_command_string = debug_command_string 159*061da546Spatrick 160*061da546Spatrick # User defined SSH options, eg, 161*061da546Spatrick # ssh.otions = dict(StrictHostKeyChecking="no",UserKnownHostsFile="/dev/null") 162*061da546Spatrick self.options = options 163*061da546Spatrick 164*061da546Spatrick def levenshtein_distance(self, a, b): 165*061da546Spatrick '''This calculates the Levenshtein distance between a and b. 166*061da546Spatrick ''' 167*061da546Spatrick 168*061da546Spatrick n, m = len(a), len(b) 169*061da546Spatrick if n > m: 170*061da546Spatrick a,b = b,a 171*061da546Spatrick n,m = m,n 172*061da546Spatrick current = range(n+1) 173*061da546Spatrick for i in range(1,m+1): 174*061da546Spatrick previous, current = current, [i]+[0]*n 175*061da546Spatrick for j in range(1,n+1): 176*061da546Spatrick add, delete = previous[j]+1, current[j-1]+1 177*061da546Spatrick change = previous[j-1] 178*061da546Spatrick if a[j-1] != b[i-1]: 179*061da546Spatrick change = change + 1 180*061da546Spatrick current[j] = min(add, delete, change) 181*061da546Spatrick return current[n] 182*061da546Spatrick 183*061da546Spatrick def try_read_prompt(self, timeout_multiplier): 184*061da546Spatrick '''This facilitates using communication timeouts to perform 185*061da546Spatrick synchronization as quickly as possible, while supporting high latency 186*061da546Spatrick connections with a tunable worst case performance. Fast connections 187*061da546Spatrick should be read almost immediately. Worst case performance for this 188*061da546Spatrick method is timeout_multiplier * 3 seconds. 189*061da546Spatrick ''' 190*061da546Spatrick 191*061da546Spatrick # maximum time allowed to read the first response 192*061da546Spatrick first_char_timeout = timeout_multiplier * 0.5 193*061da546Spatrick 194*061da546Spatrick # maximum time allowed between subsequent characters 195*061da546Spatrick inter_char_timeout = timeout_multiplier * 0.1 196*061da546Spatrick 197*061da546Spatrick # maximum time for reading the entire prompt 198*061da546Spatrick total_timeout = timeout_multiplier * 3.0 199*061da546Spatrick 200*061da546Spatrick prompt = self.string_type() 201*061da546Spatrick begin = time.time() 202*061da546Spatrick expired = 0.0 203*061da546Spatrick timeout = first_char_timeout 204*061da546Spatrick 205*061da546Spatrick while expired < total_timeout: 206*061da546Spatrick try: 207*061da546Spatrick prompt += self.read_nonblocking(size=1, timeout=timeout) 208*061da546Spatrick expired = time.time() - begin # updated total time expired 209*061da546Spatrick timeout = inter_char_timeout 210*061da546Spatrick except TIMEOUT: 211*061da546Spatrick break 212*061da546Spatrick 213*061da546Spatrick return prompt 214*061da546Spatrick 215*061da546Spatrick def sync_original_prompt (self, sync_multiplier=1.0): 216*061da546Spatrick '''This attempts to find the prompt. Basically, press enter and record 217*061da546Spatrick the response; press enter again and record the response; if the two 218*061da546Spatrick responses are similar then assume we are at the original prompt. 219*061da546Spatrick This can be a slow function. Worst case with the default sync_multiplier 220*061da546Spatrick can take 12 seconds. Low latency connections are more likely to fail 221*061da546Spatrick with a low sync_multiplier. Best case sync time gets worse with a 222*061da546Spatrick high sync multiplier (500 ms with default). ''' 223*061da546Spatrick 224*061da546Spatrick # All of these timing pace values are magic. 225*061da546Spatrick # I came up with these based on what seemed reliable for 226*061da546Spatrick # connecting to a heavily loaded machine I have. 227*061da546Spatrick self.sendline() 228*061da546Spatrick time.sleep(0.1) 229*061da546Spatrick 230*061da546Spatrick try: 231*061da546Spatrick # Clear the buffer before getting the prompt. 232*061da546Spatrick self.try_read_prompt(sync_multiplier) 233*061da546Spatrick except TIMEOUT: 234*061da546Spatrick pass 235*061da546Spatrick 236*061da546Spatrick self.sendline() 237*061da546Spatrick x = self.try_read_prompt(sync_multiplier) 238*061da546Spatrick 239*061da546Spatrick self.sendline() 240*061da546Spatrick a = self.try_read_prompt(sync_multiplier) 241*061da546Spatrick 242*061da546Spatrick self.sendline() 243*061da546Spatrick b = self.try_read_prompt(sync_multiplier) 244*061da546Spatrick 245*061da546Spatrick ld = self.levenshtein_distance(a,b) 246*061da546Spatrick len_a = len(a) 247*061da546Spatrick if len_a == 0: 248*061da546Spatrick return False 249*061da546Spatrick if float(ld)/len_a < 0.4: 250*061da546Spatrick return True 251*061da546Spatrick return False 252*061da546Spatrick 253*061da546Spatrick ### TODO: This is getting messy and I'm pretty sure this isn't perfect. 254*061da546Spatrick ### TODO: I need to draw a flow chart for this. 255*061da546Spatrick ### TODO: Unit tests for SSH tunnels, remote SSH command exec, disabling original prompt sync 256*061da546Spatrick def login (self, server, username, password='', terminal_type='ansi', 257*061da546Spatrick original_prompt=r"[#$]", login_timeout=10, port=None, 258*061da546Spatrick auto_prompt_reset=True, ssh_key=None, quiet=True, 259*061da546Spatrick sync_multiplier=1, check_local_ip=True, 260*061da546Spatrick password_regex=r'(?i)(?:password:)|(?:passphrase for key)', 261*061da546Spatrick ssh_tunnels={}, spawn_local_ssh=True, 262*061da546Spatrick sync_original_prompt=True, ssh_config=None): 263*061da546Spatrick '''This logs the user into the given server. 264*061da546Spatrick 265*061da546Spatrick It uses 266*061da546Spatrick 'original_prompt' to try to find the prompt right after login. When it 267*061da546Spatrick finds the prompt it immediately tries to reset the prompt to something 268*061da546Spatrick more easily matched. The default 'original_prompt' is very optimistic 269*061da546Spatrick and is easily fooled. It's more reliable to try to match the original 270*061da546Spatrick prompt as exactly as possible to prevent false matches by server 271*061da546Spatrick strings such as the "Message Of The Day". On many systems you can 272*061da546Spatrick disable the MOTD on the remote server by creating a zero-length file 273*061da546Spatrick called :file:`~/.hushlogin` on the remote server. If a prompt cannot be found 274*061da546Spatrick then this will not necessarily cause the login to fail. In the case of 275*061da546Spatrick a timeout when looking for the prompt we assume that the original 276*061da546Spatrick prompt was so weird that we could not match it, so we use a few tricks 277*061da546Spatrick to guess when we have reached the prompt. Then we hope for the best and 278*061da546Spatrick blindly try to reset the prompt to something more unique. If that fails 279*061da546Spatrick then login() raises an :class:`ExceptionPxssh` exception. 280*061da546Spatrick 281*061da546Spatrick In some situations it is not possible or desirable to reset the 282*061da546Spatrick original prompt. In this case, pass ``auto_prompt_reset=False`` to 283*061da546Spatrick inhibit setting the prompt to the UNIQUE_PROMPT. Remember that pxssh 284*061da546Spatrick uses a unique prompt in the :meth:`prompt` method. If the original prompt is 285*061da546Spatrick not reset then this will disable the :meth:`prompt` method unless you 286*061da546Spatrick manually set the :attr:`PROMPT` attribute. 287*061da546Spatrick 288*061da546Spatrick Set ``password_regex`` if there is a MOTD message with `password` in it. 289*061da546Spatrick Changing this is like playing in traffic, don't (p)expect it to match straight 290*061da546Spatrick away. 291*061da546Spatrick 292*061da546Spatrick If you require to connect to another SSH server from the your original SSH 293*061da546Spatrick connection set ``spawn_local_ssh`` to `False` and this will use your current 294*061da546Spatrick session to do so. Setting this option to `False` and not having an active session 295*061da546Spatrick will trigger an error. 296*061da546Spatrick 297*061da546Spatrick Set ``ssh_key`` to a file path to an SSH private key to use that SSH key 298*061da546Spatrick for the session authentication. 299*061da546Spatrick Set ``ssh_key`` to `True` to force passing the current SSH authentication socket 300*061da546Spatrick to the desired ``hostname``. 301*061da546Spatrick 302*061da546Spatrick Set ``ssh_config`` to a file path string of an SSH client config file to pass that 303*061da546Spatrick file to the client to handle itself. You may set any options you wish in here, however 304*061da546Spatrick doing so will require you to post extra information that you may not want to if you 305*061da546Spatrick run into issues. 306*061da546Spatrick ''' 307*061da546Spatrick 308*061da546Spatrick session_regex_array = ["(?i)are you sure you want to continue connecting", original_prompt, password_regex, "(?i)permission denied", "(?i)terminal type", TIMEOUT] 309*061da546Spatrick session_init_regex_array = [] 310*061da546Spatrick session_init_regex_array.extend(session_regex_array) 311*061da546Spatrick session_init_regex_array.extend(["(?i)connection closed by remote host", EOF]) 312*061da546Spatrick 313*061da546Spatrick ssh_options = ''.join([" -o '%s=%s'" % (o, v) for (o, v) in self.options.items()]) 314*061da546Spatrick if quiet: 315*061da546Spatrick ssh_options = ssh_options + ' -q' 316*061da546Spatrick if not check_local_ip: 317*061da546Spatrick ssh_options = ssh_options + " -o'NoHostAuthenticationForLocalhost=yes'" 318*061da546Spatrick if self.force_password: 319*061da546Spatrick ssh_options = ssh_options + ' ' + self.SSH_OPTS 320*061da546Spatrick if ssh_config is not None: 321*061da546Spatrick if spawn_local_ssh and not os.path.isfile(ssh_config): 322*061da546Spatrick raise ExceptionPxssh('SSH config does not exist or is not a file.') 323*061da546Spatrick ssh_options = ssh_options + '-F ' + ssh_config 324*061da546Spatrick if port is not None: 325*061da546Spatrick ssh_options = ssh_options + ' -p %s'%(str(port)) 326*061da546Spatrick if ssh_key is not None: 327*061da546Spatrick # Allow forwarding our SSH key to the current session 328*061da546Spatrick if ssh_key==True: 329*061da546Spatrick ssh_options = ssh_options + ' -A' 330*061da546Spatrick else: 331*061da546Spatrick if spawn_local_ssh and not os.path.isfile(ssh_key): 332*061da546Spatrick raise ExceptionPxssh('private ssh key does not exist or is not a file.') 333*061da546Spatrick ssh_options = ssh_options + ' -i %s' % (ssh_key) 334*061da546Spatrick 335*061da546Spatrick # SSH tunnels, make sure you know what you're putting into the lists 336*061da546Spatrick # under each heading. Do not expect these to open 100% of the time, 337*061da546Spatrick # The port you're requesting might be bound. 338*061da546Spatrick # 339*061da546Spatrick # The structure should be like this: 340*061da546Spatrick # { 'local': ['2424:localhost:22'], # Local SSH tunnels 341*061da546Spatrick # 'remote': ['2525:localhost:22'], # Remote SSH tunnels 342*061da546Spatrick # 'dynamic': [8888] } # Dynamic/SOCKS tunnels 343*061da546Spatrick if ssh_tunnels!={} and isinstance({},type(ssh_tunnels)): 344*061da546Spatrick tunnel_types = { 345*061da546Spatrick 'local':'L', 346*061da546Spatrick 'remote':'R', 347*061da546Spatrick 'dynamic':'D' 348*061da546Spatrick } 349*061da546Spatrick for tunnel_type in tunnel_types: 350*061da546Spatrick cmd_type = tunnel_types[tunnel_type] 351*061da546Spatrick if tunnel_type in ssh_tunnels: 352*061da546Spatrick tunnels = ssh_tunnels[tunnel_type] 353*061da546Spatrick for tunnel in tunnels: 354*061da546Spatrick if spawn_local_ssh==False: 355*061da546Spatrick tunnel = quote(str(tunnel)) 356*061da546Spatrick ssh_options = ssh_options + ' -' + cmd_type + ' ' + str(tunnel) 357*061da546Spatrick cmd = "ssh %s -l %s %s" % (ssh_options, username, server) 358*061da546Spatrick if self.debug_command_string: 359*061da546Spatrick return(cmd) 360*061da546Spatrick 361*061da546Spatrick # Are we asking for a local ssh command or to spawn one in another session? 362*061da546Spatrick if spawn_local_ssh: 363*061da546Spatrick spawn._spawn(self, cmd) 364*061da546Spatrick else: 365*061da546Spatrick self.sendline(cmd) 366*061da546Spatrick 367*061da546Spatrick # This does not distinguish between a remote server 'password' prompt 368*061da546Spatrick # and a local ssh 'passphrase' prompt (for unlocking a private key). 369*061da546Spatrick i = self.expect(session_init_regex_array, timeout=login_timeout) 370*061da546Spatrick 371*061da546Spatrick # First phase 372*061da546Spatrick if i==0: 373*061da546Spatrick # New certificate -- always accept it. 374*061da546Spatrick # This is what you get if SSH does not have the remote host's 375*061da546Spatrick # public key stored in the 'known_hosts' cache. 376*061da546Spatrick self.sendline("yes") 377*061da546Spatrick i = self.expect(session_regex_array) 378*061da546Spatrick if i==2: # password or passphrase 379*061da546Spatrick self.sendline(password) 380*061da546Spatrick i = self.expect(session_regex_array) 381*061da546Spatrick if i==4: 382*061da546Spatrick self.sendline(terminal_type) 383*061da546Spatrick i = self.expect(session_regex_array) 384*061da546Spatrick if i==7: 385*061da546Spatrick self.close() 386*061da546Spatrick raise ExceptionPxssh('Could not establish connection to host') 387*061da546Spatrick 388*061da546Spatrick # Second phase 389*061da546Spatrick if i==0: 390*061da546Spatrick # This is weird. This should not happen twice in a row. 391*061da546Spatrick self.close() 392*061da546Spatrick raise ExceptionPxssh('Weird error. Got "are you sure" prompt twice.') 393*061da546Spatrick elif i==1: # can occur if you have a public key pair set to authenticate. 394*061da546Spatrick ### TODO: May NOT be OK if expect() got tricked and matched a false prompt. 395*061da546Spatrick pass 396*061da546Spatrick elif i==2: # password prompt again 397*061da546Spatrick # For incorrect passwords, some ssh servers will 398*061da546Spatrick # ask for the password again, others return 'denied' right away. 399*061da546Spatrick # If we get the password prompt again then this means 400*061da546Spatrick # we didn't get the password right the first time. 401*061da546Spatrick self.close() 402*061da546Spatrick raise ExceptionPxssh('password refused') 403*061da546Spatrick elif i==3: # permission denied -- password was bad. 404*061da546Spatrick self.close() 405*061da546Spatrick raise ExceptionPxssh('permission denied') 406*061da546Spatrick elif i==4: # terminal type again? WTF? 407*061da546Spatrick self.close() 408*061da546Spatrick raise ExceptionPxssh('Weird error. Got "terminal type" prompt twice.') 409*061da546Spatrick elif i==5: # Timeout 410*061da546Spatrick #This is tricky... I presume that we are at the command-line prompt. 411*061da546Spatrick #It may be that the shell prompt was so weird that we couldn't match 412*061da546Spatrick #it. Or it may be that we couldn't log in for some other reason. I 413*061da546Spatrick #can't be sure, but it's safe to guess that we did login because if 414*061da546Spatrick #I presume wrong and we are not logged in then this should be caught 415*061da546Spatrick #later when I try to set the shell prompt. 416*061da546Spatrick pass 417*061da546Spatrick elif i==6: # Connection closed by remote host 418*061da546Spatrick self.close() 419*061da546Spatrick raise ExceptionPxssh('connection closed') 420*061da546Spatrick else: # Unexpected 421*061da546Spatrick self.close() 422*061da546Spatrick raise ExceptionPxssh('unexpected login response') 423*061da546Spatrick if sync_original_prompt: 424*061da546Spatrick if not self.sync_original_prompt(sync_multiplier): 425*061da546Spatrick self.close() 426*061da546Spatrick raise ExceptionPxssh('could not synchronize with original prompt') 427*061da546Spatrick # We appear to be in. 428*061da546Spatrick # set shell prompt to something unique. 429*061da546Spatrick if auto_prompt_reset: 430*061da546Spatrick if not self.set_unique_prompt(): 431*061da546Spatrick self.close() 432*061da546Spatrick raise ExceptionPxssh('could not set shell prompt ' 433*061da546Spatrick '(received: %r, expected: %r).' % ( 434*061da546Spatrick self.before, self.PROMPT,)) 435*061da546Spatrick return True 436*061da546Spatrick 437*061da546Spatrick def logout (self): 438*061da546Spatrick '''Sends exit to the remote shell. 439*061da546Spatrick 440*061da546Spatrick If there are stopped jobs then this automatically sends exit twice. 441*061da546Spatrick ''' 442*061da546Spatrick self.sendline("exit") 443*061da546Spatrick index = self.expect([EOF, "(?i)there are stopped jobs"]) 444*061da546Spatrick if index==1: 445*061da546Spatrick self.sendline("exit") 446*061da546Spatrick self.expect(EOF) 447*061da546Spatrick self.close() 448*061da546Spatrick 449*061da546Spatrick def prompt(self, timeout=-1): 450*061da546Spatrick '''Match the next shell prompt. 451*061da546Spatrick 452*061da546Spatrick This is little more than a short-cut to the :meth:`~pexpect.spawn.expect` 453*061da546Spatrick method. Note that if you called :meth:`login` with 454*061da546Spatrick ``auto_prompt_reset=False``, then before calling :meth:`prompt` you must 455*061da546Spatrick set the :attr:`PROMPT` attribute to a regex that it will use for 456*061da546Spatrick matching the prompt. 457*061da546Spatrick 458*061da546Spatrick Calling :meth:`prompt` will erase the contents of the :attr:`before` 459*061da546Spatrick attribute even if no prompt is ever matched. If timeout is not given or 460*061da546Spatrick it is set to -1 then self.timeout is used. 461*061da546Spatrick 462*061da546Spatrick :return: True if the shell prompt was matched, False if the timeout was 463*061da546Spatrick reached. 464*061da546Spatrick ''' 465*061da546Spatrick 466*061da546Spatrick if timeout == -1: 467*061da546Spatrick timeout = self.timeout 468*061da546Spatrick i = self.expect([self.PROMPT, TIMEOUT], timeout=timeout) 469*061da546Spatrick if i==1: 470*061da546Spatrick return False 471*061da546Spatrick return True 472*061da546Spatrick 473*061da546Spatrick def set_unique_prompt(self): 474*061da546Spatrick '''This sets the remote prompt to something more unique than ``#`` or ``$``. 475*061da546Spatrick This makes it easier for the :meth:`prompt` method to match the shell prompt 476*061da546Spatrick unambiguously. This method is called automatically by the :meth:`login` 477*061da546Spatrick method, but you may want to call it manually if you somehow reset the 478*061da546Spatrick shell prompt. For example, if you 'su' to a different user then you 479*061da546Spatrick will need to manually reset the prompt. This sends shell commands to 480*061da546Spatrick the remote host to set the prompt, so this assumes the remote host is 481*061da546Spatrick ready to receive commands. 482*061da546Spatrick 483*061da546Spatrick Alternatively, you may use your own prompt pattern. In this case you 484*061da546Spatrick should call :meth:`login` with ``auto_prompt_reset=False``; then set the 485*061da546Spatrick :attr:`PROMPT` attribute to a regular expression. After that, the 486*061da546Spatrick :meth:`prompt` method will try to match your prompt pattern. 487*061da546Spatrick ''' 488*061da546Spatrick 489*061da546Spatrick self.sendline("unset PROMPT_COMMAND") 490*061da546Spatrick self.sendline(self.PROMPT_SET_SH) # sh-style 491*061da546Spatrick i = self.expect ([TIMEOUT, self.PROMPT], timeout=10) 492*061da546Spatrick if i == 0: # csh-style 493*061da546Spatrick self.sendline(self.PROMPT_SET_CSH) 494*061da546Spatrick i = self.expect([TIMEOUT, self.PROMPT], timeout=10) 495*061da546Spatrick if i == 0: 496*061da546Spatrick return False 497*061da546Spatrick return True 498*061da546Spatrick 499*061da546Spatrick# vi:ts=4:sw=4:expandtab:ft=python: 500