blob: 9aff6c6412fbfffee147b30468752e5284f5610a [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
mblighcf965b02007-07-25 16:49:45 +0000170 def run(self, command, timeout=None, ignore_status=False):
mbligh7d2bde82007-08-02 16:26:10 +0000171 """
172 Run a command on the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000173
174 Args:
175 command: the command line string
176 timeout: time limit in seconds before attempting to
177 kill the running process. The run() function
178 will take a few seconds longer than 'timeout'
179 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000180 ignore_status: do not raise an exception, no matter
181 what the exit code of the command is.
mblighdcd57a82007-07-11 23:06:47 +0000182
183 Returns:
184 a hosts.base_classes.CmdResult object
185
186 Raises:
187 AutoservRunError: the exit code of the command
188 execution was not 0
189 """
190 #~ print "running %s" % (command,)
191 result= utils.run(r'ssh -l %s -p %d %s "%s"' % (self.user,
192 self.port, self.hostname, utils.sh_escape(command)),
mblighcf965b02007-07-25 16:49:45 +0000193 timeout, ignore_status)
mblighdcd57a82007-07-11 23:06:47 +0000194 return result
mbligh7d2bde82007-08-02 16:26:10 +0000195
196
mbligha0452c82007-08-08 20:24:57 +0000197 def reboot(self, timeout=600, label=None, kernel_args=None, wait=True):
mbligh7d2bde82007-08-02 16:26:10 +0000198 """
199 Reboot the remote host.
mbligh8b85dfb2007-08-28 09:50:31 +0000200
mbligha0452c82007-08-08 20:24:57 +0000201 Args:
202 timeout
mbligh8b85dfb2007-08-28 09:50:31 +0000203 """
mbligha0452c82007-08-08 20:24:57 +0000204 if label or kernel_args:
205 self.bootloader.install_boottool()
206 if label:
207 self.bootloader.set_default(label)
208 if kernel_args:
209 if not label:
210 default = int(self.bootloader.get_default())
211 label = self.bootloader.get_titles()[default]
212 self.bootloader.add_args(label, kernel_args)
mblighd742a222007-09-30 01:27:06 +0000213 print "Reboot: initiating reboot"
mbligha0452c82007-08-08 20:24:57 +0000214 self.run('reboot')
215 if wait:
mbligh3409ee72007-10-16 23:58:33 +0000216 self.__wait_for_restart(timeout)
mbligha0452c82007-08-08 20:24:57 +0000217
mbligh7d2bde82007-08-02 16:26:10 +0000218
mblighdcd57a82007-07-11 23:06:47 +0000219 def get_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000220 """
221 Copy files from the remote host to a local path.
mblighdcd57a82007-07-11 23:06:47 +0000222
223 Directories will be copied recursively.
224 If a source component is a directory with a trailing slash,
225 the content of the directory will be copied, otherwise, the
226 directory itself and its content will be copied. This
227 behavior is similar to that of the program 'rsync'.
228
229 Args:
230 source: either
231 1) a single file or directory, as a string
232 2) a list of one or more (possibly mixed)
233 files or directories
234 dest: a file or a directory (if source contains a
235 directory or more than one element, you must
236 supply a directory dest)
237
238 Raises:
239 AutoservRunError: the scp command failed
240 """
241 if isinstance(source, types.StringTypes):
242 source= [source]
243
244 processed_source= []
245 for entry in source:
246 if entry.endswith('/'):
247 format_string= '%s@%s:"%s*"'
248 else:
249 format_string= '%s@%s:"%s"'
250 entry= format_string % (self.user, self.hostname,
251 utils.scp_remote_escape(entry))
252 processed_source.append(entry)
253
254 processed_dest= os.path.abspath(dest)
255 if os.path.isdir(dest):
256 processed_dest= "%s/" % (utils.sh_escape(processed_dest),)
257 else:
258 processed_dest= utils.sh_escape(processed_dest)
259
260 utils.run('scp -rpq %s "%s"' % (
261 " ".join(processed_source),
262 processed_dest))
mbligh7d2bde82007-08-02 16:26:10 +0000263
264
mblighdcd57a82007-07-11 23:06:47 +0000265 def send_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000266 """
267 Copy files from a local path to the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000268
269 Directories will be copied recursively.
270 If a source component is a directory with a trailing slash,
271 the content of the directory will be copied, otherwise, the
272 directory itself and its content will be copied. This
273 behavior is similar to that of the program 'rsync'.
274
275 Args:
276 source: either
277 1) a single file or directory, as a string
278 2) a list of one or more (possibly mixed)
279 files or directories
280 dest: a file or a directory (if source contains a
281 directory or more than one element, you must
282 supply a directory dest)
283
284 Raises:
285 AutoservRunError: the scp command failed
286 """
287 if isinstance(source, types.StringTypes):
288 source= [source]
289
290 processed_source= []
291 for entry in source:
292 if entry.endswith('/'):
293 format_string= '"%s/"*'
294 else:
295 format_string= '"%s"'
296 entry= format_string % (utils.sh_escape(os.path.abspath(entry)),)
297 processed_source.append(entry)
mbligh7d2bde82007-08-02 16:26:10 +0000298
mbligh47c54002007-09-10 18:31:02 +0000299 result= utils.run('ssh -l %s %s rsync -h' % (self.user,
300 self.hostname),
301 ignore_status=True)
mblighd5669092007-08-27 19:01:05 +0000302
303 if result.exit_status == 0:
mblighd6dc1fc2007-09-11 21:21:02 +0000304 utils.run('rsync --rsh=ssh -az %s %s@%s:"%s"' % (
mblighd5669092007-08-27 19:01:05 +0000305 " ".join(processed_source), self.user,
306 self.hostname, utils.scp_remote_escape(dest)))
307 else:
308 utils.run('scp -rpq %s %s@%s:"%s"' % (
309 " ".join(processed_source), self.user,
310 self.hostname, utils.scp_remote_escape(dest)))
mbligh7d2bde82007-08-02 16:26:10 +0000311
mblighdcd57a82007-07-11 23:06:47 +0000312 def get_tmp_dir(self):
mbligh7d2bde82007-08-02 16:26:10 +0000313 """
314 Return the pathname of a directory on the host suitable
mblighdcd57a82007-07-11 23:06:47 +0000315 for temporary file storage.
316
317 The directory and its content will be deleted automatically
318 on the destruction of the Host object that was used to obtain
319 it.
320 """
mbligha25b29e2007-08-26 13:58:04 +0000321 dir_name= self.run("mktemp -d /tmp/autoserv-XXXXXX").stdout.rstrip(" \n")
mblighdcd57a82007-07-11 23:06:47 +0000322 self.tmp_dirs.append(dir_name)
323 return dir_name
mbligh7d2bde82007-08-02 16:26:10 +0000324
325
mblighdcd57a82007-07-11 23:06:47 +0000326 def is_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000327 """
328 Check if the remote host is up.
mblighdcd57a82007-07-11 23:06:47 +0000329
330 Returns:
331 True if the remote host is up, False otherwise
332 """
333 try:
334 result= self.run("true", timeout=10)
335 except errors.AutoservRunError:
336 return False
337 else:
338 if result.exit_status == 0:
339 return True
340 else:
mbligh7d2bde82007-08-02 16:26:10 +0000341
mblighdcd57a82007-07-11 23:06:47 +0000342 return False
mbligh7d2bde82007-08-02 16:26:10 +0000343
mblighdcd57a82007-07-11 23:06:47 +0000344 def wait_up(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000345 """
346 Wait until the remote host is up or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000347
348 In fact, it will wait until an ssh connection to the remote
349 host can be established.
350
351 Args:
352 timeout: time limit in seconds before returning even
353 if the host is not up.
354
355 Returns:
356 True if the host was found to be up, False otherwise
357 """
358 if timeout:
359 end_time= time.time() + timeout
360
361 while not timeout or time.time() < end_time:
362 try:
mblighe9cf9d42007-08-31 08:56:00 +0000363 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000364 result= self.run("true", timeout=run_timeout)
365 except errors.AutoservRunError:
366 pass
367 else:
368 if result.exit_status == 0:
369 return True
370 time.sleep(1)
371
372 return False
mbligh7d2bde82007-08-02 16:26:10 +0000373
374
mblighdcd57a82007-07-11 23:06:47 +0000375 def wait_down(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000376 """
377 Wait until the remote host is down or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000378
379 In fact, it will wait until an ssh connection to the remote
380 host fails.
381
382 Args:
383 timeout: time limit in seconds before returning even
384 if the host is not up.
385
386 Returns:
387 True if the host was found to be down, False otherwise
388 """
389 if timeout:
390 end_time= time.time() + timeout
391
392 while not timeout or time.time() < end_time:
393 try:
mbligh7e1e9642007-07-31 18:00:45 +0000394 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000395 result= self.run("true", timeout=run_timeout)
396 except errors.AutoservRunError:
397 return True
398 else:
399 if result.aborted:
400 return True
401 time.sleep(1)
402
403 return False
mbligh7d2bde82007-08-02 16:26:10 +0000404
405
mblighdbe4a382007-07-26 19:41:28 +0000406 def ensure_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000407 """
408 Ensure the host is up if it is not then do not proceed;
409 this prevents cacading failures of tests
410 """
mbligha0452c82007-08-08 20:24:57 +0000411 print 'Ensuring that %s is up before continuing' % self.hostname
412 if hasattr(self, 'hardreset') and not self.wait_up(300):
mblighdbe4a382007-07-26 19:41:28 +0000413 print "Performing a hardreset on %s" % self.hostname
414 self.hardreset()
415 self.wait_up()
mbligha0452c82007-08-08 20:24:57 +0000416 print 'Host up, continuing'
mbligh7d2bde82007-08-02 16:26:10 +0000417
418
mblighdcd57a82007-07-11 23:06:47 +0000419 def get_num_cpu(self):
mbligh7d2bde82007-08-02 16:26:10 +0000420 """
421 Get the number of CPUs in the host according to
mblighdcd57a82007-07-11 23:06:47 +0000422 /proc/cpuinfo.
423
424 Returns:
425 The number of CPUs
426 """
427
mbligh5f876ad2007-10-12 23:59:53 +0000428 proc_cpuinfo = self.run("cat /proc/cpuinfo").stdout
mblighdcd57a82007-07-11 23:06:47 +0000429 cpus = 0
430 for line in proc_cpuinfo.splitlines():
431 if line.startswith('processor'):
432 cpus += 1
433 return cpus
mbligh5f876ad2007-10-12 23:59:53 +0000434
435
436 def check_uptime(self):
437 """
438 Check that uptime is available and monotonically increasing.
439 """
440 if not self.ping():
441 raise "Client is not pingable"
442 result = self.run("/bin/cat /proc/uptime", 30)
443 return result.stdout.strip().split()[0]
444
445
446 def get_arch(self):
447 """
448 Get the hardware architecture of the remote machine
449 """
450 arch = self.run('/bin/uname -m').stdout.rstrip()
451 if re.match(r'i\d86$', arch):
452 arch = 'i386'
453 return arch
454
455
456 def get_kernel_ver(self):
457 """
458 Get the kernel version of the remote machine
459 """
460 return self.run('/bin/uname -r').stdout.rstrip()
461
462
463 def get_cmdline(self):
464 """
465 Get the kernel command line of the remote machine
466 """
467 return self.run('cat /proc/cmdline').stdout.rstrip()
468
469
470 def ping(self):
471 """
472 Ping the remote system, and return whether it's available
473 """
474 fpingcmd = "%s -q %s" % ('/usr/bin/fping', self.hostname)
475 rc = utils.system(fpingcmd, ignore_status = 1)
476 return (rc == 0)