blob: eb4a3015f801cc2acaac4507dbc5eb263301b2c5 [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
5"""This module defines the SSHHost class.
6
7Implementation details:
8You should import the "hosts" package instead of importing each type of host.
9
10 SSHHost: a remote machine with a ssh access
11"""
12
13__author__ = """mbligh@google.com (Martin J. Bligh),
14poirier@google.com (Benjamin Poirier),
15stutsman@google.com (Ryan Stutsman)"""
16
17
18import types
19import os
20import time
21
22import base_classes
23import utils
24import errors
25
26
27class SSHHost(base_classes.RemoteHost):
28 """This class represents a remote machine controlled through an ssh
29 session on which you can run programs.
30
31 It is not the machine autoserv is running on. The machine must be
32 configured for password-less login, for example through public key
33 authentication.
34
35 Implementation details:
36 This is a leaf class in an abstract class hierarchy, it must
37 implement the unimplemented methods in parent classes.
38 """
39
40 def __init__(self, hostname, user="root", port=22):
41 """Construct a SSHHost object
42
43 Args:
44 hostname: network hostname or address of remote machine
45 user: user to log in as on the remote machine
46 port: port the ssh daemon is listening on on the remote
47 machine
48 """
49 super(SSHHost, self).__init__()
50
51 self.hostname= hostname
52 self.user= user
53 self.port= port
54 self.tmp_dirs= []
55
56 def __del__(self):
57 """Destroy a SSHHost object
58 """
59 for dir in self.tmp_dirs:
60 try:
61 self.run('rm -rf "%s"' % (utils.sh_escape(dir)))
62 except errors.AutoservRunError:
63 pass
64
65 def run(self, command, timeout=None):
66 """Run a command on the remote host.
67
68 Args:
69 command: the command line string
70 timeout: time limit in seconds before attempting to
71 kill the running process. The run() function
72 will take a few seconds longer than 'timeout'
73 to complete if it has to kill the process.
74
75 Returns:
76 a hosts.base_classes.CmdResult object
77
78 Raises:
79 AutoservRunError: the exit code of the command
80 execution was not 0
81 """
82 #~ print "running %s" % (command,)
83 result= utils.run(r'ssh -l %s -p %d %s "%s"' % (self.user,
84 self.port, self.hostname, utils.sh_escape(command)),
85 timeout)
86 return result
87
88 def reboot(self):
89 """Reboot the remote host.
90
91 TODO(poirier): Should the function return only after having
92 done a self.wait_down()? or should this be left to
93 the control file?
94 pro: A common usage pattern would be reboot(),
95 wait_down(), wait_up(), [more commands]. If wait_down()
96 is not there, wait_up() is likely to return right away
97 because the ssh daemon has not yet shutdown, so a
98 control file expecting the host to have rebooted will
99 run eronously. Doing the wait_down() in reboot
100 eliminates the risk of confusion. Also, making the
101 wait_down() external might lead to race conditions if
102 the control file does a reboot() does some other things,
103 then there's no way to know if it should wait_down()
104 first or wait_up() right away.
105 con: wait_down() just after reboot will be mandatory,
106 this might be undesirable if there are other operations
107 that can be executed right after the reboot, for
108 example many hosts have to be rebooted at the same
109 time. The solution to this is to use multiple
110 threads of execution in the control file.
111 """
112 self.run("reboot")
113 self.wait_down()
114
115 def get_file(self, source, dest):
116 """Copy files from the remote host to a local path.
117
118 Directories will be copied recursively.
119 If a source component is a directory with a trailing slash,
120 the content of the directory will be copied, otherwise, the
121 directory itself and its content will be copied. This
122 behavior is similar to that of the program 'rsync'.
123
124 Args:
125 source: either
126 1) a single file or directory, as a string
127 2) a list of one or more (possibly mixed)
128 files or directories
129 dest: a file or a directory (if source contains a
130 directory or more than one element, you must
131 supply a directory dest)
132
133 Raises:
134 AutoservRunError: the scp command failed
135 """
136 if isinstance(source, types.StringTypes):
137 source= [source]
138
139 processed_source= []
140 for entry in source:
141 if entry.endswith('/'):
142 format_string= '%s@%s:"%s*"'
143 else:
144 format_string= '%s@%s:"%s"'
145 entry= format_string % (self.user, self.hostname,
146 utils.scp_remote_escape(entry))
147 processed_source.append(entry)
148
149 processed_dest= os.path.abspath(dest)
150 if os.path.isdir(dest):
151 processed_dest= "%s/" % (utils.sh_escape(processed_dest),)
152 else:
153 processed_dest= utils.sh_escape(processed_dest)
154
155 utils.run('scp -rpq %s "%s"' % (
156 " ".join(processed_source),
157 processed_dest))
158
159 def send_file(self, source, dest):
160 """Copy files from a local path to the remote host.
161
162 Directories will be copied recursively.
163 If a source component is a directory with a trailing slash,
164 the content of the directory will be copied, otherwise, the
165 directory itself and its content will be copied. This
166 behavior is similar to that of the program 'rsync'.
167
168 Args:
169 source: either
170 1) a single file or directory, as a string
171 2) a list of one or more (possibly mixed)
172 files or directories
173 dest: a file or a directory (if source contains a
174 directory or more than one element, you must
175 supply a directory dest)
176
177 Raises:
178 AutoservRunError: the scp command failed
179 """
180 if isinstance(source, types.StringTypes):
181 source= [source]
182
183 processed_source= []
184 for entry in source:
185 if entry.endswith('/'):
186 format_string= '"%s/"*'
187 else:
188 format_string= '"%s"'
189 entry= format_string % (utils.sh_escape(os.path.abspath(entry)),)
190 processed_source.append(entry)
191
192 utils.run('scp -rpq %s %s@%s:"%s"' % (
193 " ".join(processed_source), self.user, self.hostname,
194 utils.scp_remote_escape(dest)))
195
196 def get_tmp_dir(self):
197 """Return the pathname of a directory on the host suitable
198 for temporary file storage.
199
200 The directory and its content will be deleted automatically
201 on the destruction of the Host object that was used to obtain
202 it.
203 """
204 dir_name= self.run("mktemp -dt autoserv-XXXXXX").stdout.rstrip(" \n")
205 self.tmp_dirs.append(dir_name)
206 return dir_name
207
208 def is_up(self):
209 """Check if the remote host is up.
210
211 Returns:
212 True if the remote host is up, False otherwise
213 """
214 try:
215 result= self.run("true", timeout=10)
216 except errors.AutoservRunError:
217 return False
218 else:
219 if result.exit_status == 0:
220 return True
221 else:
222 return False
223
224 def wait_up(self, timeout=None):
225 """Wait until the remote host is up or the timeout expires.
226
227 In fact, it will wait until an ssh connection to the remote
228 host can be established.
229
230 Args:
231 timeout: time limit in seconds before returning even
232 if the host is not up.
233
234 Returns:
235 True if the host was found to be up, False otherwise
236 """
237 if timeout:
238 end_time= time.time() + timeout
239
240 while not timeout or time.time() < end_time:
241 try:
242 if timeout:
243 run_timeout= end_time - time.time()
244 else:
245 run_timeout= 10
246 result= self.run("true", timeout=run_timeout)
247 except errors.AutoservRunError:
248 pass
249 else:
250 if result.exit_status == 0:
251 return True
252 time.sleep(1)
253
254 return False
255
256 def wait_down(self, timeout=None):
257 """Wait until the remote host is down or the timeout expires.
258
259 In fact, it will wait until an ssh connection to the remote
260 host fails.
261
262 Args:
263 timeout: time limit in seconds before returning even
264 if the host is not up.
265
266 Returns:
267 True if the host was found to be down, False otherwise
268 """
269 if timeout:
270 end_time= time.time() + timeout
271
272 while not timeout or time.time() < end_time:
273 try:
274 if timeout:
275 run_timeout= end_time - time.time()
276 else:
277 run_timeout= 10
278 result= self.run("true", timeout=run_timeout)
279 except errors.AutoservRunError:
280 return True
281 else:
282 if result.aborted:
283 return True
284 time.sleep(1)
285
286 return False
287
288 def get_num_cpu(self):
289 """Get the number of CPUs in the host according to
290 /proc/cpuinfo.
291
292 Returns:
293 The number of CPUs
294 """
295
296 proc_cpuinfo= self.run("cat /proc/cpuinfo").stdout
297 cpus = 0
298 for line in proc_cpuinfo.splitlines():
299 if line.startswith('processor'):
300 cpus += 1
301 return cpus