blob: 05526f4df5d9227804f42dc552bac13ad0231215 [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
mblighd5669092007-08-27 19:01:05 +0000208 result= utils.run('ssh %s rsync -h' % self.hostname,
209 ignore_status=True)
210
211 if result.exit_status == 0:
212 utils.run('rsync --rsh=ssh -avz %s %s@%s:"%s"' % (
213 " ".join(processed_source), self.user,
214 self.hostname, utils.scp_remote_escape(dest)))
215 else:
216 utils.run('scp -rpq %s %s@%s:"%s"' % (
217 " ".join(processed_source), self.user,
218 self.hostname, utils.scp_remote_escape(dest)))
mbligh7d2bde82007-08-02 16:26:10 +0000219
mblighdcd57a82007-07-11 23:06:47 +0000220 def get_tmp_dir(self):
mbligh7d2bde82007-08-02 16:26:10 +0000221 """
222 Return the pathname of a directory on the host suitable
mblighdcd57a82007-07-11 23:06:47 +0000223 for temporary file storage.
224
225 The directory and its content will be deleted automatically
226 on the destruction of the Host object that was used to obtain
227 it.
228 """
mbligha25b29e2007-08-26 13:58:04 +0000229 dir_name= self.run("mktemp -d /tmp/autoserv-XXXXXX").stdout.rstrip(" \n")
mblighdcd57a82007-07-11 23:06:47 +0000230 self.tmp_dirs.append(dir_name)
231 return dir_name
mbligh7d2bde82007-08-02 16:26:10 +0000232
233
mblighdcd57a82007-07-11 23:06:47 +0000234 def is_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000235 """
236 Check if the remote host is up.
mblighdcd57a82007-07-11 23:06:47 +0000237
238 Returns:
239 True if the remote host is up, False otherwise
240 """
241 try:
242 result= self.run("true", timeout=10)
243 except errors.AutoservRunError:
244 return False
245 else:
246 if result.exit_status == 0:
247 return True
248 else:
mbligh7d2bde82007-08-02 16:26:10 +0000249
mblighdcd57a82007-07-11 23:06:47 +0000250 return False
mbligh7d2bde82007-08-02 16:26:10 +0000251
mblighdcd57a82007-07-11 23:06:47 +0000252 def wait_up(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000253 """
254 Wait until the remote host is up or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000255
256 In fact, it will wait until an ssh connection to the remote
257 host can be established.
258
259 Args:
260 timeout: time limit in seconds before returning even
261 if the host is not up.
262
263 Returns:
264 True if the host was found to be up, False otherwise
265 """
266 if timeout:
267 end_time= time.time() + timeout
268
269 while not timeout or time.time() < end_time:
270 try:
271 if timeout:
272 run_timeout= end_time - time.time()
273 else:
274 run_timeout= 10
275 result= self.run("true", timeout=run_timeout)
276 except errors.AutoservRunError:
277 pass
278 else:
279 if result.exit_status == 0:
280 return True
281 time.sleep(1)
282
283 return False
mbligh7d2bde82007-08-02 16:26:10 +0000284
285
mblighdcd57a82007-07-11 23:06:47 +0000286 def wait_down(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000287 """
288 Wait until the remote host is down or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000289
290 In fact, it will wait until an ssh connection to the remote
291 host fails.
292
293 Args:
294 timeout: time limit in seconds before returning even
295 if the host is not up.
296
297 Returns:
298 True if the host was found to be down, False otherwise
299 """
300 if timeout:
301 end_time= time.time() + timeout
302
303 while not timeout or time.time() < end_time:
304 try:
mbligh7e1e9642007-07-31 18:00:45 +0000305 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000306 result= self.run("true", timeout=run_timeout)
307 except errors.AutoservRunError:
308 return True
309 else:
310 if result.aborted:
311 return True
312 time.sleep(1)
313
314 return False
mbligh7d2bde82007-08-02 16:26:10 +0000315
316
mblighdbe4a382007-07-26 19:41:28 +0000317 def ensure_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000318 """
319 Ensure the host is up if it is not then do not proceed;
320 this prevents cacading failures of tests
321 """
mbligha0452c82007-08-08 20:24:57 +0000322 print 'Ensuring that %s is up before continuing' % self.hostname
323 if hasattr(self, 'hardreset') and not self.wait_up(300):
mblighdbe4a382007-07-26 19:41:28 +0000324 print "Performing a hardreset on %s" % self.hostname
325 self.hardreset()
326 self.wait_up()
mbligha0452c82007-08-08 20:24:57 +0000327 print 'Host up, continuing'
mbligh7d2bde82007-08-02 16:26:10 +0000328
329
mblighdcd57a82007-07-11 23:06:47 +0000330 def get_num_cpu(self):
mbligh7d2bde82007-08-02 16:26:10 +0000331 """
332 Get the number of CPUs in the host according to
mblighdcd57a82007-07-11 23:06:47 +0000333 /proc/cpuinfo.
334
335 Returns:
336 The number of CPUs
337 """
338
339 proc_cpuinfo= self.run("cat /proc/cpuinfo").stdout
340 cpus = 0
341 for line in proc_cpuinfo.splitlines():
342 if line.startswith('processor'):
343 cpus += 1
344 return cpus