blob: 3c8ddf8455e173daa6f6f16bac8501469f411e6e [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
28
29
30class SSHHost(base_classes.RemoteHost):
mbligh7d2bde82007-08-02 16:26:10 +000031 """
32 This class represents a remote machine controlled through an ssh
mblighdcd57a82007-07-11 23:06:47 +000033 session on which you can run programs.
mbligh7d2bde82007-08-02 16:26:10 +000034
mblighdcd57a82007-07-11 23:06:47 +000035 It is not the machine autoserv is running on. The machine must be
36 configured for password-less login, for example through public key
37 authentication.
mbligh7d2bde82007-08-02 16:26:10 +000038
mblighdcd57a82007-07-11 23:06:47 +000039 Implementation details:
40 This is a leaf class in an abstract class hierarchy, it must
41 implement the unimplemented methods in parent classes.
42 """
mbligh7d2bde82007-08-02 16:26:10 +000043
mblighdcd57a82007-07-11 23:06:47 +000044 def __init__(self, hostname, user="root", port=22):
mbligh7d2bde82007-08-02 16:26:10 +000045 """
46 Construct a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000047
48 Args:
49 hostname: network hostname or address of remote machine
50 user: user to log in as on the remote machine
51 port: port the ssh daemon is listening on on the remote
52 machine
53 """
54 super(SSHHost, self).__init__()
55
56 self.hostname= hostname
57 self.user= user
58 self.port= port
59 self.tmp_dirs= []
mbligh7d2bde82007-08-02 16:26:10 +000060
61
mblighdcd57a82007-07-11 23:06:47 +000062 def __del__(self):
mbligh7d2bde82007-08-02 16:26:10 +000063 """
64 Destroy a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000065 """
66 for dir in self.tmp_dirs:
67 try:
68 self.run('rm -rf "%s"' % (utils.sh_escape(dir)))
69 except errors.AutoservRunError:
70 pass
mbligh7d2bde82007-08-02 16:26:10 +000071
72
mblighcf965b02007-07-25 16:49:45 +000073 def run(self, command, timeout=None, ignore_status=False):
mbligh7d2bde82007-08-02 16:26:10 +000074 """
75 Run a command on the remote host.
mblighdcd57a82007-07-11 23:06:47 +000076
77 Args:
78 command: the command line string
79 timeout: time limit in seconds before attempting to
80 kill the running process. The run() function
81 will take a few seconds longer than 'timeout'
82 to complete if it has to kill the process.
83
84 Returns:
85 a hosts.base_classes.CmdResult object
86
87 Raises:
88 AutoservRunError: the exit code of the command
89 execution was not 0
90 """
91 #~ print "running %s" % (command,)
92 result= utils.run(r'ssh -l %s -p %d %s "%s"' % (self.user,
93 self.port, self.hostname, utils.sh_escape(command)),
mblighcf965b02007-07-25 16:49:45 +000094 timeout, ignore_status)
mblighdcd57a82007-07-11 23:06:47 +000095 return result
mbligh7d2bde82007-08-02 16:26:10 +000096
97
mblighdcd57a82007-07-11 23:06:47 +000098 def reboot(self):
mbligh7d2bde82007-08-02 16:26:10 +000099 """
100 Reboot the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000101
102 TODO(poirier): Should the function return only after having
103 done a self.wait_down()? or should this be left to
104 the control file?
105 pro: A common usage pattern would be reboot(),
106 wait_down(), wait_up(), [more commands]. If wait_down()
107 is not there, wait_up() is likely to return right away
108 because the ssh daemon has not yet shutdown, so a
109 control file expecting the host to have rebooted will
110 run eronously. Doing the wait_down() in reboot
111 eliminates the risk of confusion. Also, making the
112 wait_down() external might lead to race conditions if
113 the control file does a reboot() does some other things,
114 then there's no way to know if it should wait_down()
115 first or wait_up() right away.
116 con: wait_down() just after reboot will be mandatory,
117 this might be undesirable if there are other operations
118 that can be executed right after the reboot, for
119 example many hosts have to be rebooted at the same
120 time. The solution to this is to use multiple
121 threads of execution in the control file.
122 """
123 self.run("reboot")
124 self.wait_down()
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)
205
206 utils.run('scp -rpq %s %s@%s:"%s"' % (
207 " ".join(processed_source), self.user, self.hostname,
208 utils.scp_remote_escape(dest)))
mbligh7d2bde82007-08-02 16:26:10 +0000209
210
mblighdcd57a82007-07-11 23:06:47 +0000211 def get_tmp_dir(self):
mbligh7d2bde82007-08-02 16:26:10 +0000212 """
213 Return the pathname of a directory on the host suitable
mblighdcd57a82007-07-11 23:06:47 +0000214 for temporary file storage.
215
216 The directory and its content will be deleted automatically
217 on the destruction of the Host object that was used to obtain
218 it.
219 """
220 dir_name= self.run("mktemp -dt autoserv-XXXXXX").stdout.rstrip(" \n")
221 self.tmp_dirs.append(dir_name)
222 return dir_name
mbligh7d2bde82007-08-02 16:26:10 +0000223
224
mblighdcd57a82007-07-11 23:06:47 +0000225 def is_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000226 """
227 Check if the remote host is up.
mblighdcd57a82007-07-11 23:06:47 +0000228
229 Returns:
230 True if the remote host is up, False otherwise
231 """
232 try:
233 result= self.run("true", timeout=10)
234 except errors.AutoservRunError:
235 return False
236 else:
237 if result.exit_status == 0:
238 return True
239 else:
mbligh7d2bde82007-08-02 16:26:10 +0000240
mblighdcd57a82007-07-11 23:06:47 +0000241 return False
mbligh7d2bde82007-08-02 16:26:10 +0000242
mblighdcd57a82007-07-11 23:06:47 +0000243 def wait_up(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000244 """
245 Wait until the remote host is up or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000246
247 In fact, it will wait until an ssh connection to the remote
248 host can be established.
249
250 Args:
251 timeout: time limit in seconds before returning even
252 if the host is not up.
253
254 Returns:
255 True if the host was found to be up, False otherwise
256 """
257 if timeout:
258 end_time= time.time() + timeout
259
260 while not timeout or time.time() < end_time:
261 try:
262 if timeout:
263 run_timeout= end_time - time.time()
264 else:
265 run_timeout= 10
266 result= self.run("true", timeout=run_timeout)
267 except errors.AutoservRunError:
268 pass
269 else:
270 if result.exit_status == 0:
271 return True
272 time.sleep(1)
273
274 return False
mbligh7d2bde82007-08-02 16:26:10 +0000275
276
mblighdcd57a82007-07-11 23:06:47 +0000277 def wait_down(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000278 """
279 Wait until the remote host is down or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000280
281 In fact, it will wait until an ssh connection to the remote
282 host fails.
283
284 Args:
285 timeout: time limit in seconds before returning even
286 if the host is not up.
287
288 Returns:
289 True if the host was found to be down, False otherwise
290 """
291 if timeout:
292 end_time= time.time() + timeout
293
294 while not timeout or time.time() < end_time:
295 try:
mbligh7e1e9642007-07-31 18:00:45 +0000296 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000297 result= self.run("true", timeout=run_timeout)
298 except errors.AutoservRunError:
299 return True
300 else:
301 if result.aborted:
302 return True
303 time.sleep(1)
304
305 return False
mbligh7d2bde82007-08-02 16:26:10 +0000306
307
mblighdbe4a382007-07-26 19:41:28 +0000308 def ensure_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000309 """
310 Ensure the host is up if it is not then do not proceed;
311 this prevents cacading failures of tests
312 """
mblighdbe4a382007-07-26 19:41:28 +0000313 if not self.wait_up(300) and hasattr(self, 'hardreset'):
314 print "Performing a hardreset on %s" % self.hostname
315 self.hardreset()
316 self.wait_up()
mbligh7d2bde82007-08-02 16:26:10 +0000317
318
mblighdcd57a82007-07-11 23:06:47 +0000319 def get_num_cpu(self):
mbligh7d2bde82007-08-02 16:26:10 +0000320 """
321 Get the number of CPUs in the host according to
mblighdcd57a82007-07-11 23:06:47 +0000322 /proc/cpuinfo.
323
324 Returns:
325 The number of CPUs
326 """
327
328 proc_cpuinfo= self.run("cat /proc/cpuinfo").stdout
329 cpus = 0
330 for line in proc_cpuinfo.splitlines():
331 if line.startswith('processor'):
332 cpus += 1
333 return cpus