blob: a68ba13315655d3f554979a96141e04b650d00e7 [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
mbligh137a05c2007-10-04 15:56:51 +000045 def __init__(self, hostname, user="root", port=22, initialize=True):
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= []
mbligh137a05c2007-10-04 15:56:51 +000059 self.initialize = initialize
mbligh91334902007-09-28 01:47:59 +000060
61 super(SSHHost, self).__init__()
mbligha0452c82007-08-08 20:24:57 +000062 self.bootloader = bootloader.Bootloader(self)
mbligh7d2bde82007-08-02 16:26:10 +000063
64
mblighdcd57a82007-07-11 23:06:47 +000065 def __del__(self):
mbligh7d2bde82007-08-02 16:26:10 +000066 """
67 Destroy a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000068 """
69 for dir in self.tmp_dirs:
70 try:
71 self.run('rm -rf "%s"' % (utils.sh_escape(dir)))
72 except errors.AutoservRunError:
73 pass
mbligh7d2bde82007-08-02 16:26:10 +000074
75
mblighcf965b02007-07-25 16:49:45 +000076 def run(self, command, timeout=None, ignore_status=False):
mbligh7d2bde82007-08-02 16:26:10 +000077 """
78 Run a command on the remote host.
mblighdcd57a82007-07-11 23:06:47 +000079
80 Args:
81 command: the command line string
82 timeout: time limit in seconds before attempting to
83 kill the running process. The run() function
84 will take a few seconds longer than 'timeout'
85 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +000086 ignore_status: do not raise an exception, no matter
87 what the exit code of the command is.
mblighdcd57a82007-07-11 23:06:47 +000088
89 Returns:
90 a hosts.base_classes.CmdResult object
91
92 Raises:
93 AutoservRunError: the exit code of the command
94 execution was not 0
95 """
96 #~ print "running %s" % (command,)
97 result= utils.run(r'ssh -l %s -p %d %s "%s"' % (self.user,
98 self.port, self.hostname, utils.sh_escape(command)),
mblighcf965b02007-07-25 16:49:45 +000099 timeout, ignore_status)
mblighdcd57a82007-07-11 23:06:47 +0000100 return result
mbligh7d2bde82007-08-02 16:26:10 +0000101
102
mbligha0452c82007-08-08 20:24:57 +0000103 def reboot(self, timeout=600, label=None, kernel_args=None, wait=True):
mbligh7d2bde82007-08-02 16:26:10 +0000104 """
105 Reboot the remote host.
mbligh8b85dfb2007-08-28 09:50:31 +0000106
mbligha0452c82007-08-08 20:24:57 +0000107 Args:
108 timeout
mbligh8b85dfb2007-08-28 09:50:31 +0000109 """
mbligha0452c82007-08-08 20:24:57 +0000110 if label or kernel_args:
111 self.bootloader.install_boottool()
112 if label:
113 self.bootloader.set_default(label)
114 if kernel_args:
115 if not label:
116 default = int(self.bootloader.get_default())
117 label = self.bootloader.get_titles()[default]
118 self.bootloader.add_args(label, kernel_args)
mblighd742a222007-09-30 01:27:06 +0000119 print "Reboot: initiating reboot"
mbligha0452c82007-08-08 20:24:57 +0000120 self.run('reboot')
121 if wait:
122 self.wait_down(60) # Make sure he's dead, Jim
123 print "Reboot: machine has gone down"
124 self.wait_up(timeout)
125 time.sleep(2) # this is needed for complete reliability
126 self.wait_up(timeout)
127 print "Reboot complete"
128
mbligh7d2bde82007-08-02 16:26:10 +0000129
mblighdcd57a82007-07-11 23:06:47 +0000130 def get_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000131 """
132 Copy files from the remote host to a local path.
mblighdcd57a82007-07-11 23:06:47 +0000133
134 Directories will be copied recursively.
135 If a source component is a directory with a trailing slash,
136 the content of the directory will be copied, otherwise, the
137 directory itself and its content will be copied. This
138 behavior is similar to that of the program 'rsync'.
139
140 Args:
141 source: either
142 1) a single file or directory, as a string
143 2) a list of one or more (possibly mixed)
144 files or directories
145 dest: a file or a directory (if source contains a
146 directory or more than one element, you must
147 supply a directory dest)
148
149 Raises:
150 AutoservRunError: the scp command failed
151 """
152 if isinstance(source, types.StringTypes):
153 source= [source]
154
155 processed_source= []
156 for entry in source:
157 if entry.endswith('/'):
158 format_string= '%s@%s:"%s*"'
159 else:
160 format_string= '%s@%s:"%s"'
161 entry= format_string % (self.user, self.hostname,
162 utils.scp_remote_escape(entry))
163 processed_source.append(entry)
164
165 processed_dest= os.path.abspath(dest)
166 if os.path.isdir(dest):
167 processed_dest= "%s/" % (utils.sh_escape(processed_dest),)
168 else:
169 processed_dest= utils.sh_escape(processed_dest)
170
171 utils.run('scp -rpq %s "%s"' % (
172 " ".join(processed_source),
173 processed_dest))
mbligh7d2bde82007-08-02 16:26:10 +0000174
175
mblighdcd57a82007-07-11 23:06:47 +0000176 def send_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000177 """
178 Copy files from a local path to the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000179
180 Directories will be copied recursively.
181 If a source component is a directory with a trailing slash,
182 the content of the directory will be copied, otherwise, the
183 directory itself and its content will be copied. This
184 behavior is similar to that of the program 'rsync'.
185
186 Args:
187 source: either
188 1) a single file or directory, as a string
189 2) a list of one or more (possibly mixed)
190 files or directories
191 dest: a file or a directory (if source contains a
192 directory or more than one element, you must
193 supply a directory dest)
194
195 Raises:
196 AutoservRunError: the scp command failed
197 """
198 if isinstance(source, types.StringTypes):
199 source= [source]
200
201 processed_source= []
202 for entry in source:
203 if entry.endswith('/'):
204 format_string= '"%s/"*'
205 else:
206 format_string= '"%s"'
207 entry= format_string % (utils.sh_escape(os.path.abspath(entry)),)
208 processed_source.append(entry)
mbligh7d2bde82007-08-02 16:26:10 +0000209
mbligh47c54002007-09-10 18:31:02 +0000210 result= utils.run('ssh -l %s %s rsync -h' % (self.user,
211 self.hostname),
212 ignore_status=True)
mblighd5669092007-08-27 19:01:05 +0000213
214 if result.exit_status == 0:
mblighd6dc1fc2007-09-11 21:21:02 +0000215 utils.run('rsync --rsh=ssh -az %s %s@%s:"%s"' % (
mblighd5669092007-08-27 19:01:05 +0000216 " ".join(processed_source), self.user,
217 self.hostname, utils.scp_remote_escape(dest)))
218 else:
219 utils.run('scp -rpq %s %s@%s:"%s"' % (
220 " ".join(processed_source), self.user,
221 self.hostname, utils.scp_remote_escape(dest)))
mbligh7d2bde82007-08-02 16:26:10 +0000222
mblighdcd57a82007-07-11 23:06:47 +0000223 def get_tmp_dir(self):
mbligh7d2bde82007-08-02 16:26:10 +0000224 """
225 Return the pathname of a directory on the host suitable
mblighdcd57a82007-07-11 23:06:47 +0000226 for temporary file storage.
227
228 The directory and its content will be deleted automatically
229 on the destruction of the Host object that was used to obtain
230 it.
231 """
mbligha25b29e2007-08-26 13:58:04 +0000232 dir_name= self.run("mktemp -d /tmp/autoserv-XXXXXX").stdout.rstrip(" \n")
mblighdcd57a82007-07-11 23:06:47 +0000233 self.tmp_dirs.append(dir_name)
234 return dir_name
mbligh7d2bde82007-08-02 16:26:10 +0000235
236
mblighdcd57a82007-07-11 23:06:47 +0000237 def is_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000238 """
239 Check if the remote host is up.
mblighdcd57a82007-07-11 23:06:47 +0000240
241 Returns:
242 True if the remote host is up, False otherwise
243 """
244 try:
245 result= self.run("true", timeout=10)
246 except errors.AutoservRunError:
247 return False
248 else:
249 if result.exit_status == 0:
250 return True
251 else:
mbligh7d2bde82007-08-02 16:26:10 +0000252
mblighdcd57a82007-07-11 23:06:47 +0000253 return False
mbligh7d2bde82007-08-02 16:26:10 +0000254
mblighdcd57a82007-07-11 23:06:47 +0000255 def wait_up(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000256 """
257 Wait until the remote host is up or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000258
259 In fact, it will wait until an ssh connection to the remote
260 host can be established.
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 up, False otherwise
268 """
269 if timeout:
270 end_time= time.time() + timeout
271
272 while not timeout or time.time() < end_time:
273 try:
mblighe9cf9d42007-08-31 08:56:00 +0000274 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000275 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