abstract_ssh.py revision a10c660ab0509633f95ac37224b107f7daa31261
1import os, time, types, socket, shutil, glob, logging
2from autotest_lib.client.common_lib import error
3from autotest_lib.server import utils, autotest
4from autotest_lib.server.hosts import remote
5
6
7def make_ssh_command(user="root", port=22, opts='', connect_timeout=30):
8    base_command = ("/usr/bin/ssh -a -x %s -o BatchMode=yes "
9                    "-o ConnectTimeout=%d -o ServerAliveInterval=300 "
10                    "-l %s -p %d")
11    assert isinstance(connect_timeout, (int, long))
12    assert connect_timeout > 0 # can't disable the timeout
13    return base_command % (opts, connect_timeout, user, port)
14
15
16# import site specific Host class
17SiteHost = utils.import_site_class(
18    __file__, "autotest_lib.server.hosts.site_host", "SiteHost",
19    remote.RemoteHost)
20
21
22class AbstractSSHHost(SiteHost):
23    """ This class represents a generic implementation of most of the
24    framework necessary for controlling a host via ssh. It implements
25    almost all of the abstract Host methods, except for the core
26    Host.run method. """
27
28    def _initialize(self, hostname, user="root", port=22, password="",
29                    *args, **dargs):
30        super(AbstractSSHHost, self)._initialize(hostname=hostname,
31                                                 *args, **dargs)
32        self.ip = socket.getaddrinfo(self.hostname, None)[0][4][0]
33        self.user = user
34        self.port = port
35        self.password = password
36
37
38    def _encode_remote_paths(self, paths):
39        """ Given a list of file paths, encodes it as a single remote path, in
40        the style used by rsync and scp. """
41        escaped_paths = [utils.scp_remote_escape(path) for path in paths]
42        return '%s@%s:"%s"' % (self.user, self.hostname,
43                               " ".join(paths))
44
45
46    def _make_rsync_cmd(self, sources, dest, delete_dest):
47        """ Given a list of source paths and a destination path, produces the
48        appropriate rsync command for copying them. Remote paths must be
49        pre-encoded. """
50        ssh_cmd = make_ssh_command(self.user, self.port)
51        if delete_dest:
52            delete_flag = "--delete"
53        else:
54            delete_flag = ""
55        command = "rsync -L %s --timeout=1800 --rsh='%s' -az %s %s"
56        return command % (delete_flag, ssh_cmd, " ".join(sources), dest)
57
58
59    def _make_scp_cmd(self, sources, dest):
60        """ Given a list of source paths and a destination path, produces the
61        appropriate scp command for encoding it. Remote paths must be
62        pre-encoded. """
63        command = "scp -rpq -P %d %s '%s'"
64        return command % (self.port, " ".join(sources), dest)
65
66
67    def _make_rsync_compatible_globs(self, path, is_local):
68        """ Given an rsync-style path, returns a list of globbed paths
69        that will hopefully provide equivalent behaviour for scp. Does not
70        support the full range of rsync pattern matching behaviour, only that
71        exposed in the get/send_file interface (trailing slashes).
72
73        The is_local param is flag indicating if the paths should be
74        interpreted as local or remote paths. """
75
76        # non-trailing slash paths should just work
77        if len(path) == 0 or path[-1] != "/":
78            return [path]
79
80        # make a function to test if a pattern matches any files
81        if is_local:
82            def glob_matches_files(path):
83                return len(glob.glob(path)) > 0
84        else:
85            def glob_matches_files(path):
86                result = self.run("ls \"%s\"" % utils.sh_escape(path),
87                                  ignore_status=True)
88                return result.exit_status == 0
89
90        # take a set of globs that cover all files, and see which are needed
91        patterns = ["*", ".[!.]*"]
92        patterns = [p for p in patterns if glob_matches_files(path + p)]
93
94        # convert them into a set of paths suitable for the commandline
95        path = utils.sh_escape(path)
96        if is_local:
97            return ["\"%s\"%s" % (path, pattern) for pattern in patterns]
98        else:
99            return ["\"%s\"" % (path + pattern) for pattern in patterns]
100
101
102    def _make_rsync_compatible_source(self, source, is_local):
103        """ Applies the same logic as _make_rsync_compatible_globs, but
104        applies it to an entire list of sources, producing a new list of
105        sources, properly quoted. """
106        return sum((self._make_rsync_compatible_globs(path, is_local)
107                    for path in source), [])
108
109
110    def get_file(self, source, dest, delete_dest=False):
111        """
112        Copy files from the remote host to a local path.
113
114        Directories will be copied recursively.
115        If a source component is a directory with a trailing slash,
116        the content of the directory will be copied, otherwise, the
117        directory itself and its content will be copied. This
118        behavior is similar to that of the program 'rsync'.
119
120        Args:
121                source: either
122                        1) a single file or directory, as a string
123                        2) a list of one or more (possibly mixed)
124                                files or directories
125                dest: a file or a directory (if source contains a
126                        directory or more than one element, you must
127                        supply a directory dest)
128                delete_dest: if this is true, the command will also clear
129                             out any old files at dest that are not in the
130                             source
131
132        Raises:
133                AutoservRunError: the scp command failed
134        """
135        if isinstance(source, basestring):
136            source = [source]
137        dest = os.path.abspath(dest)
138
139        try:
140            remote_source = self._encode_remote_paths(source)
141            local_dest = utils.sh_escape(dest)
142            rsync = self._make_rsync_cmd([remote_source], local_dest,
143                                         delete_dest)
144            utils.run(rsync)
145        except error.CmdError, e:
146            logging.warn("warning: rsync failed with: %s", e)
147            logging.info("attempting to copy with scp instead")
148
149            # scp has no equivalent to --delete, just drop the entire dest dir
150            if delete_dest and os.path.isdir(dest):
151                shutil.rmtree(dest)
152                os.mkdir(dest)
153
154            remote_source = self._make_rsync_compatible_source(source, False)
155            if remote_source:
156                remote_source = self._encode_remote_paths(remote_source)
157                local_dest = utils.sh_escape(dest)
158                scp = self._make_scp_cmd([remote_source], local_dest)
159                try:
160                    utils.run(scp)
161                except error.CmdError, e:
162                    raise error.AutoservRunError(e.args[0], e.args[1])
163
164
165    def send_file(self, source, dest, delete_dest=False):
166        """
167        Copy files from a local path to the remote host.
168
169        Directories will be copied recursively.
170        If a source component is a directory with a trailing slash,
171        the content of the directory will be copied, otherwise, the
172        directory itself and its content will be copied. This
173        behavior is similar to that of the program 'rsync'.
174
175        Args:
176                source: either
177                        1) a single file or directory, as a string
178                        2) a list of one or more (possibly mixed)
179                                files or directories
180                dest: a file or a directory (if source contains a
181                        directory or more than one element, you must
182                        supply a directory dest)
183                delete_dest: if this is true, the command will also clear
184                             out any old files at dest that are not in the
185                             source
186
187        Raises:
188                AutoservRunError: the scp command failed
189        """
190        if isinstance(source, basestring):
191            source = [source]
192        remote_dest = self._encode_remote_paths([dest])
193
194        try:
195            local_sources = [utils.sh_escape(path) for path in source]
196            rsync = self._make_rsync_cmd(local_sources, remote_dest,
197                                         delete_dest)
198            utils.run(rsync)
199        except error.CmdError, e:
200            logging.warn("warning: rsync failed with: %s", e)
201            logging.info("attempting to copy with scp instead")
202
203            # scp has no equivalent to --delete, just drop the entire dest dir
204            if delete_dest:
205                is_dir = self.run("ls -d %s/" % remote_dest,
206                                  ignore_status=True).exit_status == 0
207                if is_dir:
208                    cmd = "rm -rf %s && mkdir %s"
209                    cmd %= (remote_dest, remote_dest)
210                    self.run(cmd)
211
212            local_sources = self._make_rsync_compatible_source(source, True)
213            if local_sources:
214                scp = self._make_scp_cmd(local_sources, remote_dest)
215                try:
216                    utils.run(scp)
217                except error.CmdError, e:
218                    raise error.AutoservRunError(e.args[0], e.args[1])
219
220        self.run('find "%s" -type d -print0 | xargs -0r chmod o+rx' % dest)
221        self.run('find "%s" -type f -print0 | xargs -0r chmod o+r' % dest)
222        if self.target_file_owner:
223            self.run('chown -R %s %s' % (self.target_file_owner, dest))
224
225
226    def ssh_ping(self, timeout=60):
227        try:
228            self.run("true", timeout=timeout, connect_timeout=timeout)
229            logging.info("ssh_ping of %s completed sucessfully", self.hostname)
230        except error.AutoservSSHTimeout:
231            msg = "ssh ping timed out (timeout = %d)" % timeout
232            raise error.AutoservSSHTimeout(msg)
233        except error.AutoservSshPermissionDeniedError:
234            #let AutoservSshPermissionDeniedError be visible to the callers
235            raise
236        except error.AutoservRunError, e:
237            msg = "command true failed in ssh ping"
238            raise error.AutoservRunError(msg, e.result_obj)
239
240
241    def is_up(self):
242        """
243        Check if the remote host is up.
244
245        Returns:
246                True if the remote host is up, False otherwise
247        """
248        try:
249            self.ssh_ping()
250        except error.AutoservError:
251            return False
252        else:
253            return True
254
255
256    def wait_up(self, timeout=None):
257        """
258        Wait until the remote host is up or the timeout expires.
259
260        In fact, it will wait until an ssh connection to the remote
261        host can be established, and getty is running.
262
263        Args:
264                timeout: time limit in seconds before returning even
265                        if the host is not up.
266
267        Returns:
268                True if the host was found to be up, False otherwise
269        """
270        if timeout:
271            end_time = time.time() + timeout
272
273        while not timeout or time.time() < end_time:
274            if self.is_up():
275                try:
276                    if self.are_wait_up_processes_up():
277                        return True
278                except error.AutoservError:
279                    pass
280            time.sleep(1)
281
282        return False
283
284
285    def wait_down(self, timeout=None):
286        """
287        Wait until the remote host is down or the timeout expires.
288
289        In fact, it will wait until an ssh connection to the remote
290        host fails.
291
292        Args:
293                timeout: time limit in seconds before returning even
294                        if the host is not up.
295
296        Returns:
297                True if the host was found to be down, False otherwise
298        """
299        if timeout:
300            end_time = time.time() + timeout
301
302        while not timeout or time.time() < end_time:
303            if not self.is_up():
304                return True
305            time.sleep(1)
306
307        return False
308
309
310    # tunable constants for the verify & repair code
311    AUTOTEST_GB_DISKSPACE_REQUIRED = 20
312
313
314    def verify(self):
315        super(AbstractSSHHost, self).verify_hardware()
316
317        logging.info('Pinging host ' + self.hostname)
318        self.ssh_ping()
319
320        if self.is_shutting_down():
321            raise error.AutoservHostError("Host is shutting down")
322
323        super(AbstractSSHHost, self).verify_software()
324
325        try:
326            autodir = autotest._get_autodir(self)
327            if autodir:
328                self.check_diskspace(autodir,
329                                     self.AUTOTEST_GB_DISKSPACE_REQUIRED)
330        except error.AutoservHostError:
331            raise           # only want to raise if it's a space issue
332        except Exception:
333            pass            # autotest dir may not exist, etc. ignore
334
335
336class LoggerFile(object):
337    def write(self, data):
338        if data:
339            logging.debug(data.rstrip("\n"))
340
341
342    def flush(self):
343        pass
344