blob: a1a0adb147fafbb9f72772eea2e7cf54fd3bbb6d [file] [log] [blame]
mblighdcd57a82007-07-11 23:06:47 +00001#!/usr/bin/python
2#
3# Copyright 2007 Google Inc. Released under the GPL v2
4
mbligh7d2bde82007-08-02 16:26:10 +00005"""
6This module defines the SSHHost class.
mblighdcd57a82007-07-11 23:06:47 +00007
8Implementation details:
9You should import the "hosts" package instead of importing each type of host.
10
11 SSHHost: a remote machine with a ssh access
12"""
13
mbligh7d2bde82007-08-02 16:26:10 +000014__author__ = """
15mbligh@google.com (Martin J. Bligh),
mblighdcd57a82007-07-11 23:06:47 +000016poirier@google.com (Benjamin Poirier),
mbligh7d2bde82007-08-02 16:26:10 +000017stutsman@google.com (Ryan Stutsman)
18"""
mblighdcd57a82007-07-11 23:06:47 +000019
20
mbligh3409ee72007-10-16 23:58:33 +000021import types, os, sys, signal, subprocess, time, re
mbligh5f876ad2007-10-12 23:59:53 +000022import base_classes, utils, errors, bootloader
mblighdcd57a82007-07-11 23:06:47 +000023
24
25class SSHHost(base_classes.RemoteHost):
mbligh7d2bde82007-08-02 16:26:10 +000026 """
27 This class represents a remote machine controlled through an ssh
mblighdcd57a82007-07-11 23:06:47 +000028 session on which you can run programs.
mbligh7d2bde82007-08-02 16:26:10 +000029
mblighdcd57a82007-07-11 23:06:47 +000030 It is not the machine autoserv is running on. The machine must be
31 configured for password-less login, for example through public key
32 authentication.
mbligh7d2bde82007-08-02 16:26:10 +000033
mbligh3409ee72007-10-16 23:58:33 +000034 It includes support for controlling the machine through a serial
35 console on which you can run programs. If such a serial console is
36 set up on the machine then capabilities such as hard reset and
37 boot strap monitoring are available. If the machine does not have a
38 serial console available then ordinary SSH-based commands will
39 still be available, but attempts to use extensions such as
40 console logging or hard reset will fail silently.
41
mblighdcd57a82007-07-11 23:06:47 +000042 Implementation details:
43 This is a leaf class in an abstract class hierarchy, it must
44 implement the unimplemented methods in parent classes.
45 """
mbligh7d2bde82007-08-02 16:26:10 +000046
mbligh3409ee72007-10-16 23:58:33 +000047 def __init__(self, hostname, user="root", port=22, initialize=True, logfilename=None, conmux_server=None, conmux_attach=None):
mbligh7d2bde82007-08-02 16:26:10 +000048 """
49 Construct a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000050
51 Args:
52 hostname: network hostname or address of remote machine
53 user: user to log in as on the remote machine
54 port: port the ssh daemon is listening on on the remote
55 machine
56 """
mblighdcd57a82007-07-11 23:06:47 +000057 self.hostname= hostname
58 self.user= user
59 self.port= port
60 self.tmp_dirs= []
mbligh137a05c2007-10-04 15:56:51 +000061 self.initialize = initialize
mbligh91334902007-09-28 01:47:59 +000062
mbligh3409ee72007-10-16 23:58:33 +000063 self.conmux_server = conmux_server
64 self.conmux_attach = self.__find_console_attach(conmux_attach)
65 self.logger_pid = None
66 self.__start_console_log(logfilename)
67
mbligh91334902007-09-28 01:47:59 +000068 super(SSHHost, self).__init__()
mbligha0452c82007-08-08 20:24:57 +000069 self.bootloader = bootloader.Bootloader(self)
mbligh7d2bde82007-08-02 16:26:10 +000070
71
mblighdcd57a82007-07-11 23:06:47 +000072 def __del__(self):
mbligh7d2bde82007-08-02 16:26:10 +000073 """
74 Destroy a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000075 """
76 for dir in self.tmp_dirs:
77 try:
78 self.run('rm -rf "%s"' % (utils.sh_escape(dir)))
79 except errors.AutoservRunError:
80 pass
mbligh3409ee72007-10-16 23:58:33 +000081 if self.logger_pid:
82 try:
83 pgid = os.getpgid(self.logger_pid)
84 os.killpg(pgid, signal.SIGTERM)
85 except OSError:
86 pass
87
88
89 def __wait_for_restart(self, timeout):
90 self.wait_down(60) # Make sure he's dead, Jim
91 self.wait_up(timeout)
92 time.sleep(2) # this is needed for complete reliability
93 self.wait_up(timeout)
94 print "Reboot complete"
95
96
97 def hardreset(self, timeout=600, wait=True):
98 """
99 Reach out and slap the box in the power switch
100 """
101 result = self.__console_run(r"'~$hardreset'")
102 if wait:
103 self.__wait_for_restart(timeout)
104 return result
105
106
107 def __start_console_log(self, logfilename):
108 """
109 Log the output of the console session to a specified file
110 """
111 if logfilename == None:
112 return
113 if not self.conmux_attach or not os.path.exists(self.conmux_attach):
114 return
115 if self.conmux_server:
116 to = '%s/%s' % (self.conmux_server, self.hostname)
117 else:
118 to = self.hostname
119 cmd = [self.conmux_attach, to, 'cat - > %s' % logfilename]
120 logger = subprocess.Popen(cmd,
121 stderr=open('/dev/null', 'w'),
122 preexec_fn=lambda: os.setpgid(0, 0))
123 self.logger_pid = logger.pid
124
125
126 def __find_console_attach(self, conmux_attach):
127 if conmux_attach:
128 return conmux_attach
129 try:
130 res = utils.run('which conmux-attach')
131 if res.exit_status == 0:
132 return res.stdout.strip()
133 except errors.AutoservRunError, e:
134 pass
135 autoserv_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
136 autotest_conmux = os.path.join(autoserv_dir, '..',
137 'conmux', 'conmux-attach')
138 autotest_conmux_alt = os.path.join(autoserv_dir,
139 '..', 'autotest',
140 'conmux', 'conmux-attach')
141 locations = [autotest_conmux,
142 autotest_conmux_alt,
143 '/usr/local/conmux/bin/conmux-attach',
144 '/usr/bin/conmux-attach']
145 for l in locations:
146 if os.path.exists(l):
147 return l
148
149 print "WARNING: conmux-attach not found on autoserv server"
150 return None
151
152
153 def __console_run(self, cmd):
154 """
155 Send a command to the conmux session
156 """
157 if not self.conmux_attach or not os.path.exists(self.conmux_attach):
158 return False
159 if self.conmux_server:
160 to = '%s/%s' % (self.conmux_server, self.hostname)
161 else:
162 to = self.hostname
163 cmd = '%s %s echo %s 2> /dev/null' % (self.conmux_attach,
164 to,
165 cmd)
166 result = os.system(cmd)
167 return result == 0
mbligh7d2bde82007-08-02 16:26:10 +0000168
169
mblighe6647d12007-10-17 00:00:01 +0000170 def ssh_command(self):
171 """Construct an ssh command with proper args for this host."""
172 return r'ssh -a -l %s -p %d %s' % (self.user,
173 self.port,
174 self.hostname)
175
176
mblighcf965b02007-07-25 16:49:45 +0000177 def run(self, command, timeout=None, ignore_status=False):
mbligh7d2bde82007-08-02 16:26:10 +0000178 """
179 Run a command on the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000180
181 Args:
182 command: the command line string
183 timeout: time limit in seconds before attempting to
184 kill the running process. The run() function
185 will take a few seconds longer than 'timeout'
186 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000187 ignore_status: do not raise an exception, no matter
188 what the exit code of the command is.
mblighdcd57a82007-07-11 23:06:47 +0000189
190 Returns:
191 a hosts.base_classes.CmdResult object
192
193 Raises:
194 AutoservRunError: the exit code of the command
195 execution was not 0
196 """
197 #~ print "running %s" % (command,)
mblighe6647d12007-10-17 00:00:01 +0000198 result= utils.run(r'%s "%s"' % (self.ssh_command(),
199 utils.sh_escape(command)),
200 timeout, ignore_status)
mblighdcd57a82007-07-11 23:06:47 +0000201 return result
mbligh7d2bde82007-08-02 16:26:10 +0000202
203
mbligha0452c82007-08-08 20:24:57 +0000204 def reboot(self, timeout=600, label=None, kernel_args=None, wait=True):
mbligh7d2bde82007-08-02 16:26:10 +0000205 """
206 Reboot the remote host.
mbligh8b85dfb2007-08-28 09:50:31 +0000207
mbligha0452c82007-08-08 20:24:57 +0000208 Args:
209 timeout
mbligh8b85dfb2007-08-28 09:50:31 +0000210 """
mbligha0452c82007-08-08 20:24:57 +0000211 if label or kernel_args:
212 self.bootloader.install_boottool()
213 if label:
214 self.bootloader.set_default(label)
215 if kernel_args:
216 if not label:
217 default = int(self.bootloader.get_default())
218 label = self.bootloader.get_titles()[default]
219 self.bootloader.add_args(label, kernel_args)
mblighd742a222007-09-30 01:27:06 +0000220 print "Reboot: initiating reboot"
mbligha0452c82007-08-08 20:24:57 +0000221 self.run('reboot')
222 if wait:
mbligh3409ee72007-10-16 23:58:33 +0000223 self.__wait_for_restart(timeout)
mbligha0452c82007-08-08 20:24:57 +0000224
mbligh7d2bde82007-08-02 16:26:10 +0000225
mblighdcd57a82007-07-11 23:06:47 +0000226 def get_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000227 """
228 Copy files from the remote host to a local path.
mblighdcd57a82007-07-11 23:06:47 +0000229
230 Directories will be copied recursively.
231 If a source component is a directory with a trailing slash,
232 the content of the directory will be copied, otherwise, the
233 directory itself and its content will be copied. This
234 behavior is similar to that of the program 'rsync'.
235
236 Args:
237 source: either
238 1) a single file or directory, as a string
239 2) a list of one or more (possibly mixed)
240 files or directories
241 dest: a file or a directory (if source contains a
242 directory or more than one element, you must
243 supply a directory dest)
244
245 Raises:
246 AutoservRunError: the scp command failed
247 """
248 if isinstance(source, types.StringTypes):
249 source= [source]
250
251 processed_source= []
252 for entry in source:
253 if entry.endswith('/'):
254 format_string= '%s@%s:"%s*"'
255 else:
256 format_string= '%s@%s:"%s"'
257 entry= format_string % (self.user, self.hostname,
258 utils.scp_remote_escape(entry))
259 processed_source.append(entry)
260
261 processed_dest= os.path.abspath(dest)
262 if os.path.isdir(dest):
263 processed_dest= "%s/" % (utils.sh_escape(processed_dest),)
264 else:
265 processed_dest= utils.sh_escape(processed_dest)
266
267 utils.run('scp -rpq %s "%s"' % (
268 " ".join(processed_source),
269 processed_dest))
mbligh7d2bde82007-08-02 16:26:10 +0000270
271
mblighdcd57a82007-07-11 23:06:47 +0000272 def send_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000273 """
274 Copy files from a local path to the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000275
276 Directories will be copied recursively.
277 If a source component is a directory with a trailing slash,
278 the content of the directory will be copied, otherwise, the
279 directory itself and its content will be copied. This
280 behavior is similar to that of the program 'rsync'.
281
282 Args:
283 source: either
284 1) a single file or directory, as a string
285 2) a list of one or more (possibly mixed)
286 files or directories
287 dest: a file or a directory (if source contains a
288 directory or more than one element, you must
289 supply a directory dest)
290
291 Raises:
292 AutoservRunError: the scp command failed
293 """
294 if isinstance(source, types.StringTypes):
295 source= [source]
296
297 processed_source= []
298 for entry in source:
299 if entry.endswith('/'):
300 format_string= '"%s/"*'
301 else:
302 format_string= '"%s"'
303 entry= format_string % (utils.sh_escape(os.path.abspath(entry)),)
304 processed_source.append(entry)
mbligh7d2bde82007-08-02 16:26:10 +0000305
mblighe6647d12007-10-17 00:00:01 +0000306 result = utils.run(r'%s rsync -h' % self.ssh_command(),
307 ignore_status=True)
mblighd5669092007-08-27 19:01:05 +0000308
309 if result.exit_status == 0:
mblighd6dc1fc2007-09-11 21:21:02 +0000310 utils.run('rsync --rsh=ssh -az %s %s@%s:"%s"' % (
mblighd5669092007-08-27 19:01:05 +0000311 " ".join(processed_source), self.user,
312 self.hostname, utils.scp_remote_escape(dest)))
313 else:
314 utils.run('scp -rpq %s %s@%s:"%s"' % (
315 " ".join(processed_source), self.user,
316 self.hostname, utils.scp_remote_escape(dest)))
mbligh7d2bde82007-08-02 16:26:10 +0000317
mblighdcd57a82007-07-11 23:06:47 +0000318 def get_tmp_dir(self):
mbligh7d2bde82007-08-02 16:26:10 +0000319 """
320 Return the pathname of a directory on the host suitable
mblighdcd57a82007-07-11 23:06:47 +0000321 for temporary file storage.
322
323 The directory and its content will be deleted automatically
324 on the destruction of the Host object that was used to obtain
325 it.
326 """
mbligha25b29e2007-08-26 13:58:04 +0000327 dir_name= self.run("mktemp -d /tmp/autoserv-XXXXXX").stdout.rstrip(" \n")
mblighdcd57a82007-07-11 23:06:47 +0000328 self.tmp_dirs.append(dir_name)
329 return dir_name
mbligh7d2bde82007-08-02 16:26:10 +0000330
331
mblighdcd57a82007-07-11 23:06:47 +0000332 def is_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000333 """
334 Check if the remote host is up.
mblighdcd57a82007-07-11 23:06:47 +0000335
336 Returns:
337 True if the remote host is up, False otherwise
338 """
339 try:
340 result= self.run("true", timeout=10)
341 except errors.AutoservRunError:
342 return False
343 else:
344 if result.exit_status == 0:
345 return True
346 else:
mbligh7d2bde82007-08-02 16:26:10 +0000347
mblighdcd57a82007-07-11 23:06:47 +0000348 return False
mbligh7d2bde82007-08-02 16:26:10 +0000349
mblighdcd57a82007-07-11 23:06:47 +0000350 def wait_up(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000351 """
352 Wait until the remote host is up or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000353
354 In fact, it will wait until an ssh connection to the remote
355 host can be established.
356
357 Args:
358 timeout: time limit in seconds before returning even
359 if the host is not up.
360
361 Returns:
362 True if the host was found to be up, False otherwise
363 """
364 if timeout:
365 end_time= time.time() + timeout
366
367 while not timeout or time.time() < end_time:
368 try:
mblighe9cf9d42007-08-31 08:56:00 +0000369 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000370 result= self.run("true", timeout=run_timeout)
371 except errors.AutoservRunError:
372 pass
373 else:
374 if result.exit_status == 0:
375 return True
376 time.sleep(1)
377
378 return False
mbligh7d2bde82007-08-02 16:26:10 +0000379
380
mblighdcd57a82007-07-11 23:06:47 +0000381 def wait_down(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000382 """
383 Wait until the remote host is down or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000384
385 In fact, it will wait until an ssh connection to the remote
386 host fails.
387
388 Args:
389 timeout: time limit in seconds before returning even
390 if the host is not up.
391
392 Returns:
393 True if the host was found to be down, False otherwise
394 """
395 if timeout:
396 end_time= time.time() + timeout
397
398 while not timeout or time.time() < end_time:
399 try:
mbligh7e1e9642007-07-31 18:00:45 +0000400 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000401 result= self.run("true", timeout=run_timeout)
402 except errors.AutoservRunError:
403 return True
404 else:
405 if result.aborted:
406 return True
407 time.sleep(1)
408
409 return False
mbligh7d2bde82007-08-02 16:26:10 +0000410
411
mblighdbe4a382007-07-26 19:41:28 +0000412 def ensure_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000413 """
414 Ensure the host is up if it is not then do not proceed;
415 this prevents cacading failures of tests
416 """
mbligha0452c82007-08-08 20:24:57 +0000417 print 'Ensuring that %s is up before continuing' % self.hostname
418 if hasattr(self, 'hardreset') and not self.wait_up(300):
mblighdbe4a382007-07-26 19:41:28 +0000419 print "Performing a hardreset on %s" % self.hostname
420 self.hardreset()
421 self.wait_up()
mbligha0452c82007-08-08 20:24:57 +0000422 print 'Host up, continuing'
mbligh7d2bde82007-08-02 16:26:10 +0000423
424
mblighdcd57a82007-07-11 23:06:47 +0000425 def get_num_cpu(self):
mbligh7d2bde82007-08-02 16:26:10 +0000426 """
427 Get the number of CPUs in the host according to
mblighdcd57a82007-07-11 23:06:47 +0000428 /proc/cpuinfo.
429
430 Returns:
431 The number of CPUs
432 """
433
mbligh5f876ad2007-10-12 23:59:53 +0000434 proc_cpuinfo = self.run("cat /proc/cpuinfo").stdout
mblighdcd57a82007-07-11 23:06:47 +0000435 cpus = 0
436 for line in proc_cpuinfo.splitlines():
437 if line.startswith('processor'):
438 cpus += 1
439 return cpus
mbligh5f876ad2007-10-12 23:59:53 +0000440
441
442 def check_uptime(self):
443 """
444 Check that uptime is available and monotonically increasing.
445 """
446 if not self.ping():
447 raise "Client is not pingable"
448 result = self.run("/bin/cat /proc/uptime", 30)
449 return result.stdout.strip().split()[0]
450
451
452 def get_arch(self):
453 """
454 Get the hardware architecture of the remote machine
455 """
456 arch = self.run('/bin/uname -m').stdout.rstrip()
457 if re.match(r'i\d86$', arch):
458 arch = 'i386'
459 return arch
460
461
462 def get_kernel_ver(self):
463 """
464 Get the kernel version of the remote machine
465 """
466 return self.run('/bin/uname -r').stdout.rstrip()
467
468
469 def get_cmdline(self):
470 """
471 Get the kernel command line of the remote machine
472 """
473 return self.run('cat /proc/cmdline').stdout.rstrip()
474
475
476 def ping(self):
477 """
478 Ping the remote system, and return whether it's available
479 """
480 fpingcmd = "%s -q %s" % ('/usr/bin/fping', self.hostname)
481 rc = utils.system(fpingcmd, ignore_status = 1)
482 return (rc == 0)