blob: 463546f08984c6df7c5e4a1d1ef094ccac2b40ff [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
mbligh5f876ad2007-10-12 23:59:53 +000021import types, os, time, re
22import 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
mblighdcd57a82007-07-11 23:06:47 +000034 Implementation details:
35 This is a leaf class in an abstract class hierarchy, it must
36 implement the unimplemented methods in parent classes.
37 """
mbligh7d2bde82007-08-02 16:26:10 +000038
mbligh137a05c2007-10-04 15:56:51 +000039 def __init__(self, hostname, user="root", port=22, initialize=True):
mbligh7d2bde82007-08-02 16:26:10 +000040 """
41 Construct a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000042
43 Args:
44 hostname: network hostname or address of remote machine
45 user: user to log in as on the remote machine
46 port: port the ssh daemon is listening on on the remote
47 machine
48 """
mblighdcd57a82007-07-11 23:06:47 +000049 self.hostname= hostname
50 self.user= user
51 self.port= port
52 self.tmp_dirs= []
mbligh137a05c2007-10-04 15:56:51 +000053 self.initialize = initialize
mbligh91334902007-09-28 01:47:59 +000054
55 super(SSHHost, self).__init__()
mbligha0452c82007-08-08 20:24:57 +000056 self.bootloader = bootloader.Bootloader(self)
mbligh7d2bde82007-08-02 16:26:10 +000057
58
mblighdcd57a82007-07-11 23:06:47 +000059 def __del__(self):
mbligh7d2bde82007-08-02 16:26:10 +000060 """
61 Destroy a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000062 """
63 for dir in self.tmp_dirs:
64 try:
65 self.run('rm -rf "%s"' % (utils.sh_escape(dir)))
66 except errors.AutoservRunError:
67 pass
mbligh7d2bde82007-08-02 16:26:10 +000068
69
mblighcf965b02007-07-25 16:49:45 +000070 def run(self, command, timeout=None, ignore_status=False):
mbligh7d2bde82007-08-02 16:26:10 +000071 """
72 Run a command on the remote host.
mblighdcd57a82007-07-11 23:06:47 +000073
74 Args:
75 command: the command line string
76 timeout: time limit in seconds before attempting to
77 kill the running process. The run() function
78 will take a few seconds longer than 'timeout'
79 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +000080 ignore_status: do not raise an exception, no matter
81 what the exit code of the command is.
mblighdcd57a82007-07-11 23:06:47 +000082
83 Returns:
84 a hosts.base_classes.CmdResult object
85
86 Raises:
87 AutoservRunError: the exit code of the command
88 execution was not 0
89 """
90 #~ print "running %s" % (command,)
91 result= utils.run(r'ssh -l %s -p %d %s "%s"' % (self.user,
92 self.port, self.hostname, utils.sh_escape(command)),
mblighcf965b02007-07-25 16:49:45 +000093 timeout, ignore_status)
mblighdcd57a82007-07-11 23:06:47 +000094 return result
mbligh7d2bde82007-08-02 16:26:10 +000095
96
mbligha0452c82007-08-08 20:24:57 +000097 def reboot(self, timeout=600, label=None, kernel_args=None, wait=True):
mbligh7d2bde82007-08-02 16:26:10 +000098 """
99 Reboot the remote host.
mbligh8b85dfb2007-08-28 09:50:31 +0000100
mbligha0452c82007-08-08 20:24:57 +0000101 Args:
102 timeout
mbligh8b85dfb2007-08-28 09:50:31 +0000103 """
mbligha0452c82007-08-08 20:24:57 +0000104 if label or kernel_args:
105 self.bootloader.install_boottool()
106 if label:
107 self.bootloader.set_default(label)
108 if kernel_args:
109 if not label:
110 default = int(self.bootloader.get_default())
111 label = self.bootloader.get_titles()[default]
112 self.bootloader.add_args(label, kernel_args)
mblighd742a222007-09-30 01:27:06 +0000113 print "Reboot: initiating reboot"
mbligha0452c82007-08-08 20:24:57 +0000114 self.run('reboot')
115 if wait:
116 self.wait_down(60) # Make sure he's dead, Jim
117 print "Reboot: machine has gone down"
118 self.wait_up(timeout)
119 time.sleep(2) # this is needed for complete reliability
120 self.wait_up(timeout)
121 print "Reboot complete"
122
mbligh7d2bde82007-08-02 16:26:10 +0000123
mblighdcd57a82007-07-11 23:06:47 +0000124 def get_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000125 """
126 Copy files from the remote host to a local path.
mblighdcd57a82007-07-11 23:06:47 +0000127
128 Directories will be copied recursively.
129 If a source component is a directory with a trailing slash,
130 the content of the directory will be copied, otherwise, the
131 directory itself and its content will be copied. This
132 behavior is similar to that of the program 'rsync'.
133
134 Args:
135 source: either
136 1) a single file or directory, as a string
137 2) a list of one or more (possibly mixed)
138 files or directories
139 dest: a file or a directory (if source contains a
140 directory or more than one element, you must
141 supply a directory dest)
142
143 Raises:
144 AutoservRunError: the scp command failed
145 """
146 if isinstance(source, types.StringTypes):
147 source= [source]
148
149 processed_source= []
150 for entry in source:
151 if entry.endswith('/'):
152 format_string= '%s@%s:"%s*"'
153 else:
154 format_string= '%s@%s:"%s"'
155 entry= format_string % (self.user, self.hostname,
156 utils.scp_remote_escape(entry))
157 processed_source.append(entry)
158
159 processed_dest= os.path.abspath(dest)
160 if os.path.isdir(dest):
161 processed_dest= "%s/" % (utils.sh_escape(processed_dest),)
162 else:
163 processed_dest= utils.sh_escape(processed_dest)
164
165 utils.run('scp -rpq %s "%s"' % (
166 " ".join(processed_source),
167 processed_dest))
mbligh7d2bde82007-08-02 16:26:10 +0000168
169
mblighdcd57a82007-07-11 23:06:47 +0000170 def send_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000171 """
172 Copy files from a local path to the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000173
174 Directories will be copied recursively.
175 If a source component is a directory with a trailing slash,
176 the content of the directory will be copied, otherwise, the
177 directory itself and its content will be copied. This
178 behavior is similar to that of the program 'rsync'.
179
180 Args:
181 source: either
182 1) a single file or directory, as a string
183 2) a list of one or more (possibly mixed)
184 files or directories
185 dest: a file or a directory (if source contains a
186 directory or more than one element, you must
187 supply a directory dest)
188
189 Raises:
190 AutoservRunError: the scp command failed
191 """
192 if isinstance(source, types.StringTypes):
193 source= [source]
194
195 processed_source= []
196 for entry in source:
197 if entry.endswith('/'):
198 format_string= '"%s/"*'
199 else:
200 format_string= '"%s"'
201 entry= format_string % (utils.sh_escape(os.path.abspath(entry)),)
202 processed_source.append(entry)
mbligh7d2bde82007-08-02 16:26:10 +0000203
mbligh47c54002007-09-10 18:31:02 +0000204 result= utils.run('ssh -l %s %s rsync -h' % (self.user,
205 self.hostname),
206 ignore_status=True)
mblighd5669092007-08-27 19:01:05 +0000207
208 if result.exit_status == 0:
mblighd6dc1fc2007-09-11 21:21:02 +0000209 utils.run('rsync --rsh=ssh -az %s %s@%s:"%s"' % (
mblighd5669092007-08-27 19:01:05 +0000210 " ".join(processed_source), self.user,
211 self.hostname, utils.scp_remote_escape(dest)))
212 else:
213 utils.run('scp -rpq %s %s@%s:"%s"' % (
214 " ".join(processed_source), self.user,
215 self.hostname, utils.scp_remote_escape(dest)))
mbligh7d2bde82007-08-02 16:26:10 +0000216
mblighdcd57a82007-07-11 23:06:47 +0000217 def get_tmp_dir(self):
mbligh7d2bde82007-08-02 16:26:10 +0000218 """
219 Return the pathname of a directory on the host suitable
mblighdcd57a82007-07-11 23:06:47 +0000220 for temporary file storage.
221
222 The directory and its content will be deleted automatically
223 on the destruction of the Host object that was used to obtain
224 it.
225 """
mbligha25b29e2007-08-26 13:58:04 +0000226 dir_name= self.run("mktemp -d /tmp/autoserv-XXXXXX").stdout.rstrip(" \n")
mblighdcd57a82007-07-11 23:06:47 +0000227 self.tmp_dirs.append(dir_name)
228 return dir_name
mbligh7d2bde82007-08-02 16:26:10 +0000229
230
mblighdcd57a82007-07-11 23:06:47 +0000231 def is_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000232 """
233 Check if the remote host is up.
mblighdcd57a82007-07-11 23:06:47 +0000234
235 Returns:
236 True if the remote host is up, False otherwise
237 """
238 try:
239 result= self.run("true", timeout=10)
240 except errors.AutoservRunError:
241 return False
242 else:
243 if result.exit_status == 0:
244 return True
245 else:
mbligh7d2bde82007-08-02 16:26:10 +0000246
mblighdcd57a82007-07-11 23:06:47 +0000247 return False
mbligh7d2bde82007-08-02 16:26:10 +0000248
mblighdcd57a82007-07-11 23:06:47 +0000249 def wait_up(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000250 """
251 Wait until the remote host is up or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000252
253 In fact, it will wait until an ssh connection to the remote
254 host can be established.
255
256 Args:
257 timeout: time limit in seconds before returning even
258 if the host is not up.
259
260 Returns:
261 True if the host was found to be up, False otherwise
262 """
263 if timeout:
264 end_time= time.time() + timeout
265
266 while not timeout or time.time() < end_time:
267 try:
mblighe9cf9d42007-08-31 08:56:00 +0000268 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000269 result= self.run("true", timeout=run_timeout)
270 except errors.AutoservRunError:
271 pass
272 else:
273 if result.exit_status == 0:
274 return True
275 time.sleep(1)
276
277 return False
mbligh7d2bde82007-08-02 16:26:10 +0000278
279
mblighdcd57a82007-07-11 23:06:47 +0000280 def wait_down(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000281 """
282 Wait until the remote host is down or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000283
284 In fact, it will wait until an ssh connection to the remote
285 host fails.
286
287 Args:
288 timeout: time limit in seconds before returning even
289 if the host is not up.
290
291 Returns:
292 True if the host was found to be down, False otherwise
293 """
294 if timeout:
295 end_time= time.time() + timeout
296
297 while not timeout or time.time() < end_time:
298 try:
mbligh7e1e9642007-07-31 18:00:45 +0000299 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000300 result= self.run("true", timeout=run_timeout)
301 except errors.AutoservRunError:
302 return True
303 else:
304 if result.aborted:
305 return True
306 time.sleep(1)
307
308 return False
mbligh7d2bde82007-08-02 16:26:10 +0000309
310
mblighdbe4a382007-07-26 19:41:28 +0000311 def ensure_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000312 """
313 Ensure the host is up if it is not then do not proceed;
314 this prevents cacading failures of tests
315 """
mbligha0452c82007-08-08 20:24:57 +0000316 print 'Ensuring that %s is up before continuing' % self.hostname
317 if hasattr(self, 'hardreset') and not self.wait_up(300):
mblighdbe4a382007-07-26 19:41:28 +0000318 print "Performing a hardreset on %s" % self.hostname
319 self.hardreset()
320 self.wait_up()
mbligha0452c82007-08-08 20:24:57 +0000321 print 'Host up, continuing'
mbligh7d2bde82007-08-02 16:26:10 +0000322
323
mblighdcd57a82007-07-11 23:06:47 +0000324 def get_num_cpu(self):
mbligh7d2bde82007-08-02 16:26:10 +0000325 """
326 Get the number of CPUs in the host according to
mblighdcd57a82007-07-11 23:06:47 +0000327 /proc/cpuinfo.
328
329 Returns:
330 The number of CPUs
331 """
332
mbligh5f876ad2007-10-12 23:59:53 +0000333 proc_cpuinfo = self.run("cat /proc/cpuinfo").stdout
mblighdcd57a82007-07-11 23:06:47 +0000334 cpus = 0
335 for line in proc_cpuinfo.splitlines():
336 if line.startswith('processor'):
337 cpus += 1
338 return cpus
mbligh5f876ad2007-10-12 23:59:53 +0000339
340
341 def check_uptime(self):
342 """
343 Check that uptime is available and monotonically increasing.
344 """
345 if not self.ping():
346 raise "Client is not pingable"
347 result = self.run("/bin/cat /proc/uptime", 30)
348 return result.stdout.strip().split()[0]
349
350
351 def get_arch(self):
352 """
353 Get the hardware architecture of the remote machine
354 """
355 arch = self.run('/bin/uname -m').stdout.rstrip()
356 if re.match(r'i\d86$', arch):
357 arch = 'i386'
358 return arch
359
360
361 def get_kernel_ver(self):
362 """
363 Get the kernel version of the remote machine
364 """
365 return self.run('/bin/uname -r').stdout.rstrip()
366
367
368 def get_cmdline(self):
369 """
370 Get the kernel command line of the remote machine
371 """
372 return self.run('cat /proc/cmdline').stdout.rstrip()
373
374
375 def ping(self):
376 """
377 Ping the remote system, and return whether it's available
378 """
379 fpingcmd = "%s -q %s" % ('/usr/bin/fping', self.hostname)
380 rc = utils.system(fpingcmd, ignore_status = 1)
381 return (rc == 0)