jadmanski | ca7da37 | 2008-10-21 16:26:52 +0000 | [diff] [blame] | 1 | import os, time, types |
| 2 | from autotest_lib.client.common_lib import error |
| 3 | from autotest_lib.server import utils |
| 4 | from autotest_lib.server.hosts import site_host |
| 5 | |
| 6 | |
jadmanski | ca7da37 | 2008-10-21 16:26:52 +0000 | [diff] [blame] | 7 | def 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 " |
| 10 | "-o ServerAliveInterval=300 " |
| 11 | "-l %s -p %d") |
| 12 | assert isinstance(connect_timeout, (int, long)) |
| 13 | assert connect_timeout > 0 # can't disable the timeout |
| 14 | return base_command % (opts, connect_timeout, user, port) |
| 15 | |
| 16 | |
| 17 | class AbstractSSHHost(site_host.SiteHost): |
| 18 | """ This class represents a generic implementation of most of the |
| 19 | framework necessary for controlling a host via ssh. It implements |
| 20 | almost all of the abstract Host methods, except for the core |
| 21 | Host.run method. """ |
| 22 | |
jadmanski | f656291 | 2008-10-21 17:59:01 +0000 | [diff] [blame^] | 23 | def _initialize(self, hostname, user="root", port=22, password="", |
| 24 | *args, **dargs): |
| 25 | super(AbstractSSHHost, self)._initialize(hostname=hostname, |
| 26 | *args, **dargs) |
jadmanski | ca7da37 | 2008-10-21 16:26:52 +0000 | [diff] [blame] | 27 | |
| 28 | self.user = user |
| 29 | self.port = port |
| 30 | self.password = password |
| 31 | |
| 32 | |
| 33 | def _copy_files(self, sources, dest): |
| 34 | """ |
| 35 | Copy files from one machine to another. |
| 36 | |
| 37 | This is for internal use by other methods that intend to move |
| 38 | files between machines. It expects a list of source files and |
| 39 | a destination (a filename if the source is a single file, a |
| 40 | destination otherwise). The names must already be |
| 41 | pre-processed into the appropriate rsync/scp friendly |
| 42 | format (%s@%s:%s). |
| 43 | """ |
| 44 | |
| 45 | print '_copy_files: copying %s to %s' % (sources, dest) |
| 46 | try: |
| 47 | ssh = make_ssh_command(self.user, self.port) |
| 48 | command = "rsync -L --rsh='%s' -az %s %s" |
| 49 | command %= (ssh, " ".join(sources), dest) |
| 50 | utils.run(command) |
| 51 | except Exception, e: |
| 52 | print "warning: rsync failed with: %s" % e |
| 53 | print "attempting to copy with scp instead" |
| 54 | try: |
| 55 | command = "scp -rpq -P %d %s '%s'" |
| 56 | command %= (self.port, ' '.join(sources), dest) |
| 57 | except error.CmdError, cmderr: |
| 58 | raise error.AutoservRunError(cmderr.args[0], cmderr.args[1]) |
| 59 | |
| 60 | |
| 61 | def get_file(self, source, dest): |
| 62 | """ |
| 63 | Copy files from the remote host to a local path. |
| 64 | |
| 65 | Directories will be copied recursively. |
| 66 | If a source component is a directory with a trailing slash, |
| 67 | the content of the directory will be copied, otherwise, the |
| 68 | directory itself and its content will be copied. This |
| 69 | behavior is similar to that of the program 'rsync'. |
| 70 | |
| 71 | Args: |
| 72 | source: either |
| 73 | 1) a single file or directory, as a string |
| 74 | 2) a list of one or more (possibly mixed) |
| 75 | files or directories |
| 76 | dest: a file or a directory (if source contains a |
| 77 | directory or more than one element, you must |
| 78 | supply a directory dest) |
| 79 | |
| 80 | Raises: |
| 81 | AutoservRunError: the scp command failed |
| 82 | """ |
| 83 | if isinstance(source, basestring): |
| 84 | source = [source] |
| 85 | |
| 86 | processed_source = [] |
| 87 | for path in source: |
| 88 | if path.endswith('/'): |
| 89 | format_string = '%s@%s:"%s*"' |
| 90 | else: |
| 91 | format_string = '%s@%s:"%s"' |
| 92 | entry = format_string % (self.user, self.hostname, |
| 93 | utils.scp_remote_escape(path)) |
| 94 | processed_source.append(entry) |
| 95 | |
| 96 | processed_dest = utils.sh_escape(os.path.abspath(dest)) |
| 97 | if os.path.isdir(dest): |
| 98 | processed_dest += "/" |
| 99 | |
| 100 | self._copy_files(processed_source, processed_dest) |
| 101 | |
| 102 | |
| 103 | def send_file(self, source, dest): |
| 104 | """ |
| 105 | Copy files from a local path to the remote host. |
| 106 | |
| 107 | Directories will be copied recursively. |
| 108 | If a source component is a directory with a trailing slash, |
| 109 | the content of the directory will be copied, otherwise, the |
| 110 | directory itself and its content will be copied. This |
| 111 | behavior is similar to that of the program 'rsync'. |
| 112 | |
| 113 | Args: |
| 114 | source: either |
| 115 | 1) a single file or directory, as a string |
| 116 | 2) a list of one or more (possibly mixed) |
| 117 | files or directories |
| 118 | dest: a file or a directory (if source contains a |
| 119 | directory or more than one element, you must |
| 120 | supply a directory dest) |
| 121 | |
| 122 | Raises: |
| 123 | AutoservRunError: the scp command failed |
| 124 | """ |
| 125 | if isinstance(source, basestring): |
| 126 | source = [source] |
| 127 | |
| 128 | processed_source = [] |
| 129 | for path in source: |
| 130 | if path.endswith('/'): |
| 131 | format_string = '"%s/"*' |
| 132 | else: |
| 133 | format_string = '"%s"' |
| 134 | entry = format_string % (utils.sh_escape(os.path.abspath(path)),) |
| 135 | processed_source.append(entry) |
| 136 | |
| 137 | remote_dest = '%s@%s:"%s"' % (self.user, self.hostname, |
| 138 | utils.scp_remote_escape(dest)) |
| 139 | |
| 140 | self._copy_files(processed_source, remote_dest) |
| 141 | self.run('find "%s" -type d -print0 | xargs -0r chmod o+rx' % dest) |
| 142 | self.run('find "%s" -type f -print0 | xargs -0r chmod o+r' % dest) |
| 143 | if self.target_file_owner: |
| 144 | self.run('chown -R %s %s' % (self.target_file_owner, dest)) |
| 145 | |
| 146 | |
| 147 | def ssh_ping(self, timeout=60): |
| 148 | try: |
| 149 | self.run("true", timeout=timeout, connect_timeout=timeout) |
| 150 | except error.AutoservSSHTimeout: |
| 151 | msg = "ssh ping timed out (timeout = %d)" % timeout |
| 152 | raise error.AutoservSSHTimeout(msg) |
| 153 | except error.AutoservRunError, e: |
| 154 | msg = "command true failed in ssh ping" |
| 155 | raise error.AutoservRunError(msg, e.result_obj) |
| 156 | |
| 157 | |
| 158 | def is_up(self): |
| 159 | """ |
| 160 | Check if the remote host is up. |
| 161 | |
| 162 | Returns: |
| 163 | True if the remote host is up, False otherwise |
| 164 | """ |
| 165 | try: |
| 166 | self.ssh_ping() |
| 167 | except error.AutoservError: |
| 168 | return False |
| 169 | else: |
| 170 | return True |
| 171 | |
| 172 | |
| 173 | def wait_up(self, timeout=None): |
| 174 | """ |
| 175 | Wait until the remote host is up or the timeout expires. |
| 176 | |
| 177 | In fact, it will wait until an ssh connection to the remote |
| 178 | host can be established, and getty is running. |
| 179 | |
| 180 | Args: |
| 181 | timeout: time limit in seconds before returning even |
| 182 | if the host is not up. |
| 183 | |
| 184 | Returns: |
| 185 | True if the host was found to be up, False otherwise |
| 186 | """ |
| 187 | if timeout: |
| 188 | end_time = time.time() + timeout |
| 189 | |
| 190 | while not timeout or time.time() < end_time: |
| 191 | if self.is_up(): |
| 192 | try: |
| 193 | if self.are_wait_up_processes_up(): |
| 194 | return True |
| 195 | except error.AutoservError: |
| 196 | pass |
| 197 | time.sleep(1) |
| 198 | |
| 199 | return False |
| 200 | |
| 201 | |
| 202 | def wait_down(self, timeout=None): |
| 203 | """ |
| 204 | Wait until the remote host is down or the timeout expires. |
| 205 | |
| 206 | In fact, it will wait until an ssh connection to the remote |
| 207 | host fails. |
| 208 | |
| 209 | Args: |
| 210 | timeout: time limit in seconds before returning even |
| 211 | if the host is not up. |
| 212 | |
| 213 | Returns: |
| 214 | True if the host was found to be down, False otherwise |
| 215 | """ |
| 216 | if timeout: |
| 217 | end_time = time.time() + timeout |
| 218 | |
| 219 | while not timeout or time.time() < end_time: |
| 220 | if not self.is_up(): |
| 221 | return True |
| 222 | time.sleep(1) |
| 223 | |
| 224 | return False |
jadmanski | f656291 | 2008-10-21 17:59:01 +0000 | [diff] [blame^] | 225 | |
| 226 | # tunable constants for the verify & repair code |
| 227 | AUTOTEST_GB_DISKSPACE_REQUIRED = 20 |
| 228 | HOURS_TO_WAIT_FOR_RECOVERY = 2.5 |
| 229 | |
| 230 | def verify(self): |
| 231 | super(AbstractSSHHost, self).verify() |
| 232 | |
| 233 | print 'Pinging host ' + self.hostname |
| 234 | self.ssh_ping() |
| 235 | |
| 236 | try: |
| 237 | autodir = autotest._get_autodir(self) |
| 238 | if autodir: |
| 239 | print 'Checking diskspace for %s on %s' % (self.hostname, |
| 240 | autodir) |
| 241 | self.check_diskspace(autodir, |
| 242 | self.AUTOTEST_GB_DISKSPACE_REQUIRED) |
| 243 | except error.AutoservHostError: |
| 244 | raise # only want to raise if it's a space issue |
| 245 | except Exception: |
| 246 | pass # autotest dir may not exist, etc. ignore |
| 247 | |
| 248 | |
| 249 | def repair_filesystem_only(self): |
| 250 | super(AbstractSSHHost, self).repair_filesystem_only() |
| 251 | self.wait_up(int(self.HOURS_TO_WAIT_FOR_RECOVERY * 3600)) |
| 252 | self.reboot() |
| 253 | |
| 254 | |
| 255 | def repair_full(self): |
| 256 | super(AbstractSSHHost, self).repair_full() |
| 257 | try: |
| 258 | self.repair_filesystem_only() |
| 259 | self.verify() |
| 260 | except Exception: |
| 261 | # the filesystem-only repair failed, try something more drastic |
| 262 | print "Filesystem-only repair failed" |
| 263 | traceback.print_exc() |
| 264 | try: |
| 265 | self.machine_install() |
| 266 | except NotImplementedError, e: |
| 267 | sys.stderr.write(str(e) + "\n\n") |