blob: 21dad1afa38f3b4fda9b9254bf922ae56808a99d [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.
85
86 Returns:
87 a hosts.base_classes.CmdResult object
88
89 Raises:
90 AutoservRunError: the exit code of the command
91 execution was not 0
92 """
93 #~ print "running %s" % (command,)
94 result= utils.run(r'ssh -l %s -p %d %s "%s"' % (self.user,
95 self.port, self.hostname, utils.sh_escape(command)),
mblighcf965b02007-07-25 16:49:45 +000096 timeout, ignore_status)
mblighdcd57a82007-07-11 23:06:47 +000097 return result
mbligh7d2bde82007-08-02 16:26:10 +000098
99
mbligha0452c82007-08-08 20:24:57 +0000100 def reboot(self, timeout=600, label=None, kernel_args=None, wait=True):
mbligh7d2bde82007-08-02 16:26:10 +0000101 """
102 Reboot the remote host.
mbligha0452c82007-08-08 20:24:57 +0000103
104 Args:
105 timeout
106 """
107 if label or kernel_args:
108 self.bootloader.install_boottool()
109 if label:
110 self.bootloader.set_default(label)
111 if kernel_args:
112 if not label:
113 default = int(self.bootloader.get_default())
114 label = self.bootloader.get_titles()[default]
115 self.bootloader.add_args(label, kernel_args)
116 self.run('reboot')
117 if wait:
118 self.wait_down(60) # Make sure he's dead, Jim
119 print "Reboot: machine has gone down"
120 self.wait_up(timeout)
121 time.sleep(2) # this is needed for complete reliability
122 self.wait_up(timeout)
123 print "Reboot complete"
124
mbligh7d2bde82007-08-02 16:26:10 +0000125
mblighdcd57a82007-07-11 23:06:47 +0000126 def get_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000127 """
128 Copy files from the remote host to a local path.
mblighdcd57a82007-07-11 23:06:47 +0000129
130 Directories will be copied recursively.
131 If a source component is a directory with a trailing slash,
132 the content of the directory will be copied, otherwise, the
133 directory itself and its content will be copied. This
134 behavior is similar to that of the program 'rsync'.
135
136 Args:
137 source: either
138 1) a single file or directory, as a string
139 2) a list of one or more (possibly mixed)
140 files or directories
141 dest: a file or a directory (if source contains a
142 directory or more than one element, you must
143 supply a directory dest)
144
145 Raises:
146 AutoservRunError: the scp command failed
147 """
148 if isinstance(source, types.StringTypes):
149 source= [source]
150
151 processed_source= []
152 for entry in source:
153 if entry.endswith('/'):
154 format_string= '%s@%s:"%s*"'
155 else:
156 format_string= '%s@%s:"%s"'
157 entry= format_string % (self.user, self.hostname,
158 utils.scp_remote_escape(entry))
159 processed_source.append(entry)
160
161 processed_dest= os.path.abspath(dest)
162 if os.path.isdir(dest):
163 processed_dest= "%s/" % (utils.sh_escape(processed_dest),)
164 else:
165 processed_dest= utils.sh_escape(processed_dest)
166
167 utils.run('scp -rpq %s "%s"' % (
168 " ".join(processed_source),
169 processed_dest))
mbligh7d2bde82007-08-02 16:26:10 +0000170
171
mblighdcd57a82007-07-11 23:06:47 +0000172 def send_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000173 """
174 Copy files from a local path to the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000175
176 Directories will be copied recursively.
177 If a source component is a directory with a trailing slash,
178 the content of the directory will be copied, otherwise, the
179 directory itself and its content will be copied. This
180 behavior is similar to that of the program 'rsync'.
181
182 Args:
183 source: either
184 1) a single file or directory, as a string
185 2) a list of one or more (possibly mixed)
186 files or directories
187 dest: a file or a directory (if source contains a
188 directory or more than one element, you must
189 supply a directory dest)
190
191 Raises:
192 AutoservRunError: the scp command failed
193 """
194 if isinstance(source, types.StringTypes):
195 source= [source]
196
197 processed_source= []
198 for entry in source:
199 if entry.endswith('/'):
200 format_string= '"%s/"*'
201 else:
202 format_string= '"%s"'
203 entry= format_string % (utils.sh_escape(os.path.abspath(entry)),)
204 processed_source.append(entry)
mbligh7d2bde82007-08-02 16:26:10 +0000205
mblighd5669092007-08-27 19:01:05 +0000206 result= utils.run('ssh %s rsync -h' % self.hostname,
207 ignore_status=True)
208
209 if result.exit_status == 0:
210 utils.run('rsync --rsh=ssh -avz %s %s@%s:"%s"' % (
211 " ".join(processed_source), self.user,
212 self.hostname, utils.scp_remote_escape(dest)))
213 else:
214 utils.run('scp -rpq %s %s@%s:"%s"' % (
215 " ".join(processed_source), self.user,
216 self.hostname, utils.scp_remote_escape(dest)))
mbligh7d2bde82007-08-02 16:26:10 +0000217
mblighdcd57a82007-07-11 23:06:47 +0000218 def get_tmp_dir(self):
mbligh7d2bde82007-08-02 16:26:10 +0000219 """
220 Return the pathname of a directory on the host suitable
mblighdcd57a82007-07-11 23:06:47 +0000221 for temporary file storage.
222
223 The directory and its content will be deleted automatically
224 on the destruction of the Host object that was used to obtain
225 it.
226 """
mbligha25b29e2007-08-26 13:58:04 +0000227 dir_name= self.run("mktemp -d /tmp/autoserv-XXXXXX").stdout.rstrip(" \n")
mblighdcd57a82007-07-11 23:06:47 +0000228 self.tmp_dirs.append(dir_name)
229 return dir_name
mbligh7d2bde82007-08-02 16:26:10 +0000230
231
mblighdcd57a82007-07-11 23:06:47 +0000232 def is_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000233 """
234 Check if the remote host is up.
mblighdcd57a82007-07-11 23:06:47 +0000235
236 Returns:
237 True if the remote host is up, False otherwise
238 """
239 try:
240 result= self.run("true", timeout=10)
241 except errors.AutoservRunError:
242 return False
243 else:
244 if result.exit_status == 0:
245 return True
246 else:
mbligh7d2bde82007-08-02 16:26:10 +0000247
mblighdcd57a82007-07-11 23:06:47 +0000248 return False
mbligh7d2bde82007-08-02 16:26:10 +0000249
mblighdcd57a82007-07-11 23:06:47 +0000250 def wait_up(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000251 """
252 Wait until the remote host is up or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000253
254 In fact, it will wait until an ssh connection to the remote
255 host can be established.
256
257 Args:
258 timeout: time limit in seconds before returning even
259 if the host is not up.
260
261 Returns:
262 True if the host was found to be up, False otherwise
263 """
264 if timeout:
265 end_time= time.time() + timeout
266
267 while not timeout or time.time() < end_time:
268 try:
269 if timeout:
270 run_timeout= end_time - time.time()
271 else:
272 run_timeout= 10
273 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