xref: /aosp_15_r20/external/autotest/client/common_lib/pxssh.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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