blob: 8a5996e13c90ead9f419f6bb9565fd223685cbe1 [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 """
mblighdcd57a82007-07-11 23:06:47 +000055 self.hostname= hostname
56 self.user= user
57 self.port= port
58 self.tmp_dirs= []
mbligh91334902007-09-28 01:47:59 +000059
60 super(SSHHost, self).__init__()
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)
mblighd742a222007-09-30 01:27:06 +0000118 print "Reboot: initiating reboot"
mbligha0452c82007-08-08 20:24:57 +0000119 self.run('reboot')
120 if wait:
121 self.wait_down(60) # Make sure he's dead, Jim
122 print "Reboot: machine has gone down"
123 self.wait_up(timeout)
124 time.sleep(2) # this is needed for complete reliability
125 self.wait_up(timeout)
126 print "Reboot complete"
127
mbligh7d2bde82007-08-02 16:26:10 +0000128
mblighdcd57a82007-07-11 23:06:47 +0000129 def get_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000130 """
131 Copy files from the remote host to a local path.
mblighdcd57a82007-07-11 23:06:47 +0000132
133 Directories will be copied recursively.
134 If a source component is a directory with a trailing slash,
135 the content of the directory will be copied, otherwise, the
136 directory itself and its content will be copied. This
137 behavior is similar to that of the program 'rsync'.
138
139 Args:
140 source: either
141 1) a single file or directory, as a string
142 2) a list of one or more (possibly mixed)
143 files or directories
144 dest: a file or a directory (if source contains a
145 directory or more than one element, you must
146 supply a directory dest)
147
148 Raises:
149 AutoservRunError: the scp command failed
150 """
151 if isinstance(source, types.StringTypes):
152 source= [source]
153
154 processed_source= []
155 for entry in source:
156 if entry.endswith('/'):
157 format_string= '%s@%s:"%s*"'
158 else:
159 format_string= '%s@%s:"%s"'
160 entry= format_string % (self.user, self.hostname,
161 utils.scp_remote_escape(entry))
162 processed_source.append(entry)
163
164 processed_dest= os.path.abspath(dest)
165 if os.path.isdir(dest):
166 processed_dest= "%s/" % (utils.sh_escape(processed_dest),)
167 else:
168 processed_dest= utils.sh_escape(processed_dest)
169
170 utils.run('scp -rpq %s "%s"' % (
171 " ".join(processed_source),
172 processed_dest))
mbligh7d2bde82007-08-02 16:26:10 +0000173
174
mblighdcd57a82007-07-11 23:06:47 +0000175 def send_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000176 """
177 Copy files from a local path to the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000178
179 Directories will be copied recursively.
180 If a source component is a directory with a trailing slash,
181 the content of the directory will be copied, otherwise, the
182 directory itself and its content will be copied. This
183 behavior is similar to that of the program 'rsync'.
184
185 Args:
186 source: either
187 1) a single file or directory, as a string
188 2) a list of one or more (possibly mixed)
189 files or directories
190 dest: a file or a directory (if source contains a
191 directory or more than one element, you must
192 supply a directory dest)
193
194 Raises:
195 AutoservRunError: the scp command failed
196 """
197 if isinstance(source, types.StringTypes):
198 source= [source]
199
200 processed_source= []
201 for entry in source:
202 if entry.endswith('/'):
203 format_string= '"%s/"*'
204 else:
205 format_string= '"%s"'
206 entry= format_string % (utils.sh_escape(os.path.abspath(entry)),)
207 processed_source.append(entry)
mbligh7d2bde82007-08-02 16:26:10 +0000208
mbligh47c54002007-09-10 18:31:02 +0000209 result= utils.run('ssh -l %s %s rsync -h' % (self.user,
210 self.hostname),
211 ignore_status=True)
mblighd5669092007-08-27 19:01:05 +0000212
213 if result.exit_status == 0:
mblighd6dc1fc2007-09-11 21:21:02 +0000214 utils.run('rsync --rsh=ssh -az %s %s@%s:"%s"' % (
mblighd5669092007-08-27 19:01:05 +0000215 " ".join(processed_source), self.user,
216 self.hostname, utils.scp_remote_escape(dest)))
217 else:
218 utils.run('scp -rpq %s %s@%s:"%s"' % (
219 " ".join(processed_source), self.user,
220 self.hostname, utils.scp_remote_escape(dest)))
mbligh7d2bde82007-08-02 16:26:10 +0000221
mblighdcd57a82007-07-11 23:06:47 +0000222 def get_tmp_dir(self):
mbligh7d2bde82007-08-02 16:26:10 +0000223 """
224 Return the pathname of a directory on the host suitable
mblighdcd57a82007-07-11 23:06:47 +0000225 for temporary file storage.
226
227 The directory and its content will be deleted automatically
228 on the destruction of the Host object that was used to obtain
229 it.
230 """
mbligha25b29e2007-08-26 13:58:04 +0000231 dir_name= self.run("mktemp -d /tmp/autoserv-XXXXXX").stdout.rstrip(" \n")
mblighdcd57a82007-07-11 23:06:47 +0000232 self.tmp_dirs.append(dir_name)
233 return dir_name
mbligh7d2bde82007-08-02 16:26:10 +0000234
235
mblighdcd57a82007-07-11 23:06:47 +0000236 def is_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000237 """
238 Check if the remote host is up.
mblighdcd57a82007-07-11 23:06:47 +0000239
240 Returns:
241 True if the remote host is up, False otherwise
242 """
243 try:
244 result= self.run("true", timeout=10)
245 except errors.AutoservRunError:
246 return False
247 else:
248 if result.exit_status == 0:
249 return True
250 else:
mbligh7d2bde82007-08-02 16:26:10 +0000251
mblighdcd57a82007-07-11 23:06:47 +0000252 return False
mbligh7d2bde82007-08-02 16:26:10 +0000253
mblighdcd57a82007-07-11 23:06:47 +0000254 def wait_up(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000255 """
256 Wait until the remote host is up or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000257
258 In fact, it will wait until an ssh connection to the remote
259 host can be established.
260
261 Args:
262 timeout: time limit in seconds before returning even
263 if the host is not up.
264
265 Returns:
266 True if the host was found to be up, False otherwise
267 """
268 if timeout:
269 end_time= time.time() + timeout
270
271 while not timeout or time.time() < end_time:
272 try:
mblighe9cf9d42007-08-31 08:56:00 +0000273 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000274 result= self.run("true", timeout=run_timeout)
275 except errors.AutoservRunError:
276 pass
277 else:
278 if result.exit_status == 0:
279 return True
280 time.sleep(1)
281
282 return False
mbligh7d2bde82007-08-02 16:26:10 +0000283
284
mblighdcd57a82007-07-11 23:06:47 +0000285 def wait_down(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000286 """
287 Wait until the remote host is down or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000288
289 In fact, it will wait until an ssh connection to the remote
290 host fails.
291
292 Args:
293 timeout: time limit in seconds before returning even
294 if the host is not up.
295
296 Returns:
297 True if the host was found to be down, False otherwise
298 """
299 if timeout:
300 end_time= time.time() + timeout
301
302 while not timeout or time.time() < end_time:
303 try:
mbligh7e1e9642007-07-31 18:00:45 +0000304 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000305 result= self.run("true", timeout=run_timeout)
306 except errors.AutoservRunError:
307 return True
308 else:
309 if result.aborted:
310 return True
311 time.sleep(1)
312
313 return False
mbligh7d2bde82007-08-02 16:26:10 +0000314
315
mblighdbe4a382007-07-26 19:41:28 +0000316 def ensure_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000317 """
318 Ensure the host is up if it is not then do not proceed;
319 this prevents cacading failures of tests
320 """
mbligha0452c82007-08-08 20:24:57 +0000321 print 'Ensuring that %s is up before continuing' % self.hostname
322 if hasattr(self, 'hardreset') and not self.wait_up(300):
mblighdbe4a382007-07-26 19:41:28 +0000323 print "Performing a hardreset on %s" % self.hostname
324 self.hardreset()
325 self.wait_up()
mbligha0452c82007-08-08 20:24:57 +0000326 print 'Host up, continuing'
mbligh7d2bde82007-08-02 16:26:10 +0000327
328
mblighdcd57a82007-07-11 23:06:47 +0000329 def get_num_cpu(self):
mbligh7d2bde82007-08-02 16:26:10 +0000330 """
331 Get the number of CPUs in the host according to
mblighdcd57a82007-07-11 23:06:47 +0000332 /proc/cpuinfo.
333
334 Returns:
335 The number of CPUs
336 """
337
338 proc_cpuinfo= self.run("cat /proc/cpuinfo").stdout
339 cpus = 0
340 for line in proc_cpuinfo.splitlines():
341 if line.startswith('processor'):
342 cpus += 1
343 return cpus