1 # 2 # Copyright 2007 Google Inc. Released under the GPL v2 3 4 """ 5 This module defines the SSHHost class. 6 7 Implementation details: 8 You should import the "hosts" package instead of importing each type of host. 9 10 SSHHost: a remote machine with a ssh access 11 """ 12 13 import re, logging 14 from autotest_lib.client.common_lib import error, pxssh 15 from autotest_lib.client.common_lib.cros.graphite import autotest_stats 16 from autotest_lib.server import utils 17 from autotest_lib.server.hosts import abstract_ssh 18 19 20 class SSHHost(abstract_ssh.AbstractSSHHost): 21 """ 22 This class represents a remote machine controlled through an ssh 23 session on which you can run programs. 24 25 It is not the machine autoserv is running on. The machine must be 26 configured for password-less login, for example through public key 27 authentication. 28 29 It includes support for controlling the machine through a serial 30 console on which you can run programs. If such a serial console is 31 set up on the machine then capabilities such as hard reset and 32 boot strap monitoring are available. If the machine does not have a 33 serial console available then ordinary SSH-based commands will 34 still be available, but attempts to use extensions such as 35 console logging or hard reset will fail silently. 36 37 Implementation details: 38 This is a leaf class in an abstract class hierarchy, it must 39 implement the unimplemented methods in parent classes. 40 """ 41 42 def _initialize(self, hostname, *args, **dargs): 43 """ 44 Construct a SSHHost object 45 46 Args: 47 hostname: network hostname or address of remote machine 48 """ 49 super(SSHHost, self)._initialize(hostname=hostname, *args, **dargs) 50 self.setup_ssh() 51 52 53 def ssh_command(self, connect_timeout=30, options='', alive_interval=300): 54 """ 55 Construct an ssh command with proper args for this host. 56 57 @param connect_timeout: connection timeout (in seconds) 58 @param options: SSH options 59 @param alive_interval: SSH Alive interval. 60 61 """ 62 options = "%s %s" % (options, self.master_ssh_option) 63 base_cmd = self.make_ssh_command(user=self.user, port=self.port, 64 opts=options, 65 hosts_file=self.known_hosts_file, 66 connect_timeout=connect_timeout, 67 alive_interval=alive_interval) 68 return "%s %s" % (base_cmd, self.hostname) 69 70 71 def _run(self, command, timeout, ignore_status, 72 stdout, stderr, connect_timeout, env, options, stdin, args, 73 ignore_timeout): 74 """Helper function for run().""" 75 ssh_cmd = self.ssh_command(connect_timeout, options) 76 if not env.strip(): 77 env = "" 78 else: 79 env = "export %s;" % env 80 for arg in args: 81 command += ' "%s"' % utils.sh_escape(arg) 82 full_cmd = '%s "%s %s"' % (ssh_cmd, env, utils.sh_escape(command)) 83 84 # TODO(jrbarnette): crbug.com/484726 - When we're in an SSP 85 # container, sometimes shortly after reboot we will see DNS 86 # resolution errors on ssh commands; the problem never 87 # occurs more than once in a row. This especially affects 88 # the autoupdate_Rollback test, but other cases have been 89 # affected, too. 90 # 91 # We work around it by detecting the first DNS resolution error 92 # and retrying exactly one time. 93 dns_retry_count = 2 94 while True: 95 result = utils.run(full_cmd, timeout, True, stdout, stderr, 96 verbose=False, stdin=stdin, 97 stderr_is_expected=ignore_status, 98 ignore_timeout=ignore_timeout) 99 dns_retry_count -= 1 100 if (result and result.exit_status == 255 and 101 re.search(r'^ssh: .*: Name or service not known', 102 result.stderr)): 103 if dns_retry_count: 104 logging.debug('Retrying because of DNS failure') 105 continue 106 logging.debug('Retry failed.') 107 autotest_stats.Counter('dns_retry_hack.fail').increment() 108 elif not dns_retry_count: 109 logging.debug('Retry succeeded.') 110 autotest_stats.Counter('dns_retry_hack.pass').increment() 111 break 112 113 if ignore_timeout and not result: 114 return None 115 116 # The error messages will show up in band (indistinguishable 117 # from stuff sent through the SSH connection), so we have the 118 # remote computer echo the message "Connected." before running 119 # any command. Since the following 2 errors have to do with 120 # connecting, it's safe to do these checks. 121 if result.exit_status == 255: 122 if re.search(r'^ssh: connect to host .* port .*: ' 123 r'Connection timed out\r$', result.stderr): 124 raise error.AutoservSSHTimeout("ssh timed out", result) 125 if "Permission denied." in result.stderr: 126 msg = "ssh permission denied" 127 raise error.AutoservSshPermissionDeniedError(msg, result) 128 129 if not ignore_status and result.exit_status > 0: 130 raise error.AutoservRunError("command execution error", result) 131 132 return result 133 134 135 def run(self, command, timeout=3600, ignore_status=False, 136 stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS, 137 connect_timeout=30, options='', stdin=None, verbose=True, args=(), 138 ignore_timeout=False): 139 """ 140 Run a command on the remote host. 141 @see common_lib.hosts.host.run() 142 143 @param connect_timeout: connection timeout (in seconds) 144 @param options: string with additional ssh command options 145 @param verbose: log the commands 146 @param ignore_timeout: bool True if SSH command timeouts should be 147 ignored. Will return None on command timeout. 148 149 @raises AutoservRunError: if the command failed 150 @raises AutoservSSHTimeout: ssh connection has timed out 151 """ 152 if verbose: 153 logging.debug("Running (ssh) '%s'", command) 154 155 # Start a master SSH connection if necessary. 156 self.start_master_ssh() 157 158 env = " ".join("=".join(pair) for pair in self.env.iteritems()) 159 try: 160 return self._run(command, timeout, ignore_status, 161 stdout_tee, stderr_tee, connect_timeout, env, 162 options, stdin, args, ignore_timeout) 163 except error.CmdError, cmderr: 164 # We get a CmdError here only if there is timeout of that command. 165 # Catch that and stuff it into AutoservRunError and raise it. 166 timeout_message = str('Timeout encountered: %s' % cmderr.args[0]) 167 raise error.AutoservRunError(timeout_message, cmderr.args[1]) 168 169 170 def run_background(self, command, verbose=True): 171 """Start a command on the host in the background. 172 173 The command is started on the host in the background, and 174 this method call returns immediately without waiting for the 175 command's completion. The PID of the process on the host is 176 returned as a string. 177 178 The command may redirect its stdin, stdout, or stderr as 179 necessary. Without redirection, all input and output will 180 use /dev/null. 181 182 @param command The command to run in the background 183 @param verbose As for `self.run()` 184 185 @return Returns the PID of the remote background process 186 as a string. 187 """ 188 # Redirection here isn't merely hygienic; it's a functional 189 # requirement. sshd won't terminate until stdin, stdout, 190 # and stderr are all closed. 191 # 192 # The subshell is needed to do the right thing in case the 193 # passed in command has its own I/O redirections. 194 cmd_fmt = '( %s ) </dev/null >/dev/null 2>&1 & echo -n $!' 195 return self.run(cmd_fmt % command, verbose=verbose).stdout 196 197 198 def run_short(self, command, **kwargs): 199 """ 200 Calls the run() command with a short default timeout. 201 202 Takes the same arguments as does run(), 203 with the exception of the timeout argument which 204 here is fixed at 60 seconds. 205 It returns the result of run. 206 207 @param command: the command line string 208 209 """ 210 return self.run(command, timeout=60, **kwargs) 211 212 213 def run_grep(self, command, timeout=30, ignore_status=False, 214 stdout_ok_regexp=None, stdout_err_regexp=None, 215 stderr_ok_regexp=None, stderr_err_regexp=None, 216 connect_timeout=30): 217 """ 218 Run a command on the remote host and look for regexp 219 in stdout or stderr to determine if the command was 220 successul or not. 221 222 223 @param command: the command line string 224 @param timeout: time limit in seconds before attempting to 225 kill the running process. The run() function 226 will take a few seconds longer than 'timeout' 227 to complete if it has to kill the process. 228 @param ignore_status: do not raise an exception, no matter 229 what the exit code of the command is. 230 @param stdout_ok_regexp: regexp that should be in stdout 231 if the command was successul. 232 @param stdout_err_regexp: regexp that should be in stdout 233 if the command failed. 234 @param stderr_ok_regexp: regexp that should be in stderr 235 if the command was successul. 236 @param stderr_err_regexp: regexp that should be in stderr 237 if the command failed. 238 @param connect_timeout: connection timeout (in seconds) 239 240 Returns: 241 if the command was successul, raises an exception 242 otherwise. 243 244 Raises: 245 AutoservRunError: 246 - the exit code of the command execution was not 0. 247 - If stderr_err_regexp is found in stderr, 248 - If stdout_err_regexp is found in stdout, 249 - If stderr_ok_regexp is not found in stderr. 250 - If stdout_ok_regexp is not found in stdout, 251 """ 252 253 # We ignore the status, because we will handle it at the end. 254 result = self.run(command, timeout, ignore_status=True, 255 connect_timeout=connect_timeout) 256 257 # Look for the patterns, in order 258 for (regexp, stream) in ((stderr_err_regexp, result.stderr), 259 (stdout_err_regexp, result.stdout)): 260 if regexp and stream: 261 err_re = re.compile (regexp) 262 if err_re.search(stream): 263 raise error.AutoservRunError( 264 '%s failed, found error pattern: "%s"' % (command, 265 regexp), result) 266 267 for (regexp, stream) in ((stderr_ok_regexp, result.stderr), 268 (stdout_ok_regexp, result.stdout)): 269 if regexp and stream: 270 ok_re = re.compile (regexp) 271 if ok_re.search(stream): 272 if ok_re.search(stream): 273 return 274 275 if not ignore_status and result.exit_status > 0: 276 raise error.AutoservRunError("command execution error", result) 277 278 279 def setup_ssh_key(self): 280 """Setup SSH Key""" 281 logging.debug('Performing SSH key setup on %s:%d as %s.', 282 self.hostname, self.port, self.user) 283 284 try: 285 host = pxssh.pxssh() 286 host.login(self.hostname, self.user, self.password, 287 port=self.port) 288 public_key = utils.get_public_key() 289 290 host.sendline('mkdir -p ~/.ssh') 291 host.prompt() 292 host.sendline('chmod 700 ~/.ssh') 293 host.prompt() 294 host.sendline("echo '%s' >> ~/.ssh/authorized_keys; " % 295 public_key) 296 host.prompt() 297 host.sendline('chmod 600 ~/.ssh/authorized_keys') 298 host.prompt() 299 host.logout() 300 301 logging.debug('SSH key setup complete.') 302 303 except: 304 logging.debug('SSH key setup has failed.') 305 try: 306 host.logout() 307 except: 308 pass 309 310 311 def setup_ssh(self): 312 """Setup SSH""" 313 if self.password: 314 try: 315 self.ssh_ping() 316 except error.AutoservSshPingHostError: 317 self.setup_ssh_key() 318