blob: fadbb87fa4d03ca06b4233d6611fe6c2ac74a796 [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
21import types
22import os
23import time
24
25import base_classes
26import utils
27import errors
mbligha0452c82007-08-08 20:24:57 +000028import bootloader
mblighdcd57a82007-07-11 23:06:47 +000029
30
31class SSHHost(base_classes.RemoteHost):
mbligh7d2bde82007-08-02 16:26:10 +000032 """
33 This class represents a remote machine controlled through an ssh
mblighdcd57a82007-07-11 23:06:47 +000034 session on which you can run programs.
mbligh7d2bde82007-08-02 16:26:10 +000035
mblighdcd57a82007-07-11 23:06:47 +000036 It is not the machine autoserv is running on. The machine must be
37 configured for password-less login, for example through public key
38 authentication.
mbligh7d2bde82007-08-02 16:26:10 +000039
mblighdcd57a82007-07-11 23:06:47 +000040 Implementation details:
41 This is a leaf class in an abstract class hierarchy, it must
42 implement the unimplemented methods in parent classes.
43 """
mbligh7d2bde82007-08-02 16:26:10 +000044
mblighdcd57a82007-07-11 23:06:47 +000045 def __init__(self, hostname, user="root", port=22):
mbligh7d2bde82007-08-02 16:26:10 +000046 """
47 Construct a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000048
49 Args:
50 hostname: network hostname or address of remote machine
51 user: user to log in as on the remote machine
52 port: port the ssh daemon is listening on on the remote
53 machine
54 """
55 super(SSHHost, self).__init__()
56
57 self.hostname= hostname
58 self.user= user
59 self.port= port
60 self.tmp_dirs= []
mbligha0452c82007-08-08 20:24:57 +000061 self.bootloader = bootloader.Bootloader(self)
mbligh7d2bde82007-08-02 16:26:10 +000062
63
mblighdcd57a82007-07-11 23:06:47 +000064 def __del__(self):
mbligh7d2bde82007-08-02 16:26:10 +000065 """
66 Destroy a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000067 """
68 for dir in self.tmp_dirs:
69 try:
70 self.run('rm -rf "%s"' % (utils.sh_escape(dir)))
71 except errors.AutoservRunError:
72 pass
mbligh7d2bde82007-08-02 16:26:10 +000073
74
mblighcf965b02007-07-25 16:49:45 +000075 def run(self, command, timeout=None, ignore_status=False):
mbligh7d2bde82007-08-02 16:26:10 +000076 """
77 Run a command on the remote host.
mblighdcd57a82007-07-11 23:06:47 +000078
79 Args:
80 command: the command line string
81 timeout: time limit in seconds before attempting to
82 kill the running process. The run() function
83 will take a few seconds longer than 'timeout'
84 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +000085 ignore_status: do not raise an exception, no matter
86 what the exit code of the command is.
mblighdcd57a82007-07-11 23:06:47 +000087
88 Returns:
89 a hosts.base_classes.CmdResult object
90
91 Raises:
92 AutoservRunError: the exit code of the command
93 execution was not 0
94 """
95 #~ print "running %s" % (command,)
96 result= utils.run(r'ssh -l %s -p %d %s "%s"' % (self.user,
97 self.port, self.hostname, utils.sh_escape(command)),
mblighcf965b02007-07-25 16:49:45 +000098 timeout, ignore_status)
mblighdcd57a82007-07-11 23:06:47 +000099 return result
mbligh7d2bde82007-08-02 16:26:10 +0000100
101
mbligha0452c82007-08-08 20:24:57 +0000102 def reboot(self, timeout=600, label=None, kernel_args=None, wait=True):
mbligh7d2bde82007-08-02 16:26:10 +0000103 """
104 Reboot the remote host.
mbligh8b85dfb2007-08-28 09:50:31 +0000105
mbligha0452c82007-08-08 20:24:57 +0000106 Args:
107 timeout
mbligh8b85dfb2007-08-28 09:50:31 +0000108 """
mbligha0452c82007-08-08 20:24:57 +0000109 if label or kernel_args:
110 self.bootloader.install_boottool()
111 if label:
112 self.bootloader.set_default(label)
113 if kernel_args:
114 if not label:
115 default = int(self.bootloader.get_default())
116 label = self.bootloader.get_titles()[default]
117 self.bootloader.add_args(label, kernel_args)
118 self.run('reboot')
119 if wait:
120 self.wait_down(60) # Make sure he's dead, Jim
121 print "Reboot: machine has gone down"
122 self.wait_up(timeout)
123 time.sleep(2) # this is needed for complete reliability
124 self.wait_up(timeout)
125 print "Reboot complete"
126
mbligh7d2bde82007-08-02 16:26:10 +0000127
mblighdcd57a82007-07-11 23:06:47 +0000128 def get_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000129 """
130 Copy files from the remote host to a local path.
mblighdcd57a82007-07-11 23:06:47 +0000131
132 Directories will be copied recursively.
133 If a source component is a directory with a trailing slash,
134 the content of the directory will be copied, otherwise, the
135 directory itself and its content will be copied. This
136 behavior is similar to that of the program 'rsync'.
137
138 Args:
139 source: either
140 1) a single file or directory, as a string
141 2) a list of one or more (possibly mixed)
142 files or directories
143 dest: a file or a directory (if source contains a
144 directory or more than one element, you must
145 supply a directory dest)
146
147 Raises:
148 AutoservRunError: the scp command failed
149 """
150 if isinstance(source, types.StringTypes):
151 source= [source]
152
153 processed_source= []
154 for entry in source:
155 if entry.endswith('/'):
156 format_string= '%s@%s:"%s*"'
157 else:
158 format_string= '%s@%s:"%s"'
159 entry= format_string % (self.user, self.hostname,
160 utils.scp_remote_escape(entry))
161 processed_source.append(entry)
162
163 processed_dest= os.path.abspath(dest)
164 if os.path.isdir(dest):
165 processed_dest= "%s/" % (utils.sh_escape(processed_dest),)
166 else:
167 processed_dest= utils.sh_escape(processed_dest)
168
169 utils.run('scp -rpq %s "%s"' % (
170 " ".join(processed_source),
171 processed_dest))
mbligh7d2bde82007-08-02 16:26:10 +0000172
173
mblighdcd57a82007-07-11 23:06:47 +0000174 def send_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000175 """
176 Copy files from a local path to the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000177
178 Directories will be copied recursively.
179 If a source component is a directory with a trailing slash,
180 the content of the directory will be copied, otherwise, the
181 directory itself and its content will be copied. This
182 behavior is similar to that of the program 'rsync'.
183
184 Args:
185 source: either
186 1) a single file or directory, as a string
187 2) a list of one or more (possibly mixed)
188 files or directories
189 dest: a file or a directory (if source contains a
190 directory or more than one element, you must
191 supply a directory dest)
192
193 Raises:
194 AutoservRunError: the scp command failed
195 """
196 if isinstance(source, types.StringTypes):
197 source= [source]
198
199 processed_source= []
200 for entry in source:
201 if entry.endswith('/'):
202 format_string= '"%s/"*'
203 else:
204 format_string= '"%s"'
205 entry= format_string % (utils.sh_escape(os.path.abspath(entry)),)
206 processed_source.append(entry)
mbligh7d2bde82007-08-02 16:26:10 +0000207
mbligh47c54002007-09-10 18:31:02 +0000208 result= utils.run('ssh -l %s %s rsync -h' % (self.user,
209 self.hostname),
210 ignore_status=True)
mblighd5669092007-08-27 19:01:05 +0000211
212 if result.exit_status == 0:
213 utils.run('rsync --rsh=ssh -avz %s %s@%s:"%s"' % (
214 " ".join(processed_source), self.user,
215 self.hostname, utils.scp_remote_escape(dest)))
216 else:
217 utils.run('scp -rpq %s %s@%s:"%s"' % (
218 " ".join(processed_source), self.user,
219 self.hostname, utils.scp_remote_escape(dest)))
mbligh7d2bde82007-08-02 16:26:10 +0000220
mblighdcd57a82007-07-11 23:06:47 +0000221 def get_tmp_dir(self):
mbligh7d2bde82007-08-02 16:26:10 +0000222 """
223 Return the pathname of a directory on the host suitable
mblighdcd57a82007-07-11 23:06:47 +0000224 for temporary file storage.
225
226 The directory and its content will be deleted automatically
227 on the destruction of the Host object that was used to obtain
228 it.
229 """
mbligha25b29e2007-08-26 13:58:04 +0000230 dir_name= self.run("mktemp -d /tmp/autoserv-XXXXXX").stdout.rstrip(" \n")
mblighdcd57a82007-07-11 23:06:47 +0000231 self.tmp_dirs.append(dir_name)
232 return dir_name
mbligh7d2bde82007-08-02 16:26:10 +0000233
234
mblighdcd57a82007-07-11 23:06:47 +0000235 def is_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000236 """
237 Check if the remote host is up.
mblighdcd57a82007-07-11 23:06:47 +0000238
239 Returns:
240 True if the remote host is up, False otherwise
241 """
242 try:
243 result= self.run("true", timeout=10)
244 except errors.AutoservRunError:
245 return False
246 else:
247 if result.exit_status == 0:
248 return True
249 else:
mbligh7d2bde82007-08-02 16:26:10 +0000250
mblighdcd57a82007-07-11 23:06:47 +0000251 return False
mbligh7d2bde82007-08-02 16:26:10 +0000252
mblighdcd57a82007-07-11 23:06:47 +0000253 def wait_up(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000254 """
255 Wait until the remote host is up or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000256
257 In fact, it will wait until an ssh connection to the remote
258 host can be established.
259
260 Args:
261 timeout: time limit in seconds before returning even
262 if the host is not up.
263
264 Returns:
265 True if the host was found to be up, False otherwise
266 """
267 if timeout:
268 end_time= time.time() + timeout
269
270 while not timeout or time.time() < end_time:
271 try:
mblighe9cf9d42007-08-31 08:56:00 +0000272 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000273 result= self.run("true", timeout=run_timeout)
274 except errors.AutoservRunError:
275 pass
276 else:
277 if result.exit_status == 0:
278 return True
279 time.sleep(1)
280
281 return False
mbligh7d2bde82007-08-02 16:26:10 +0000282
283
mblighdcd57a82007-07-11 23:06:47 +0000284 def wait_down(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000285 """
286 Wait until the remote host is down or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000287
288 In fact, it will wait until an ssh connection to the remote
289 host fails.
290
291 Args:
292 timeout: time limit in seconds before returning even
293 if the host is not up.
294
295 Returns:
296 True if the host was found to be down, False otherwise
297 """
298 if timeout:
299 end_time= time.time() + timeout
300
301 while not timeout or time.time() < end_time:
302 try:
mbligh7e1e9642007-07-31 18:00:45 +0000303 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000304 result= self.run("true", timeout=run_timeout)
305 except errors.AutoservRunError:
306 return True
307 else:
308 if result.aborted:
309 return True
310 time.sleep(1)
311
312 return False
mbligh7d2bde82007-08-02 16:26:10 +0000313
314
mblighdbe4a382007-07-26 19:41:28 +0000315 def ensure_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000316 """
317 Ensure the host is up if it is not then do not proceed;
318 this prevents cacading failures of tests
319 """
mbligha0452c82007-08-08 20:24:57 +0000320 print 'Ensuring that %s is up before continuing' % self.hostname
321 if hasattr(self, 'hardreset') and not self.wait_up(300):
mblighdbe4a382007-07-26 19:41:28 +0000322 print "Performing a hardreset on %s" % self.hostname
323 self.hardreset()
324 self.wait_up()
mbligha0452c82007-08-08 20:24:57 +0000325 print 'Host up, continuing'
mbligh7d2bde82007-08-02 16:26:10 +0000326
327
mblighdcd57a82007-07-11 23:06:47 +0000328 def get_num_cpu(self):
mbligh7d2bde82007-08-02 16:26:10 +0000329 """
330 Get the number of CPUs in the host according to
mblighdcd57a82007-07-11 23:06:47 +0000331 /proc/cpuinfo.
332
333 Returns:
334 The number of CPUs
335 """
336
337 proc_cpuinfo= self.run("cat /proc/cpuinfo").stdout
338 cpus = 0
339 for line in proc_cpuinfo.splitlines():
340 if line.startswith('processor'):
341 cpus += 1
342 return cpus