Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(18)

Side by Side Diff: tools/telemetry/telemetry/cros_interface.py

Issue 12278015: [Telemetry] Reorganize everything. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Re-add shebangs. Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4 """A wrapper around ssh for common operations on a CrOS-based device"""
5 import logging
6 import os
7 import re
8 import subprocess
9 import sys
10 import time
11 import tempfile
12
13 from telemetry import util
14
15 # TODO(nduca): This whole file is built up around making individual ssh calls
16 # for each operation. It really could get away with a single ssh session built
17 # around pexpect, I suspect, if we wanted it to be faster. But, this was
18 # convenient.
19
20 def RunCmd(args, cwd=None, quiet=False):
21 """Opens a subprocess to execute a program and returns its return value.
22
23 Args:
24 args: A string or a sequence of program arguments. The program to execute is
25 the string or the first item in the args sequence.
26 cwd: If not None, the subprocess's current directory will be changed to
27 |cwd| before it's executed.
28
29 Returns:
30 Return code from the command execution.
31 """
32 if not quiet:
33 logging.debug(' '.join(args) + ' ' + (cwd or ''))
34 with open(os.devnull, 'w') as devnull:
35 p = subprocess.Popen(args=args, cwd=cwd, stdout=devnull,
36 stderr=devnull, stdin=devnull, shell=False)
37 return p.wait()
38
39 def GetAllCmdOutput(args, cwd=None, quiet=False):
40 """Open a subprocess to execute a program and returns its output.
41
42 Args:
43 args: A string or a sequence of program arguments. The program to execute is
44 the string or the first item in the args sequence.
45 cwd: If not None, the subprocess's current directory will be changed to
46 |cwd| before it's executed.
47
48 Returns:
49 Captures and returns the command's stdout.
50 Prints the command's stderr to logger (which defaults to stdout).
51 """
52 if not quiet:
53 logging.debug(' '.join(args) + ' ' + (cwd or ''))
54 with open(os.devnull, 'w') as devnull:
55 p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE,
56 stderr=subprocess.PIPE, stdin=devnull, shell=False)
57 stdout, stderr = p.communicate()
58 if not quiet:
59 logging.debug(' > stdout=[%s], stderr=[%s]', stdout, stderr)
60 return stdout, stderr
61
62 class DeviceSideProcess(object):
63 def __init__(self,
64 cri,
65 device_side_args,
66 prevent_output=True,
67 extra_ssh_args=None,
68 leave_ssh_alive=False,
69 env=None,
70 login_shell=False):
71
72 # Init members first so that Close will always succeed.
73 self._cri = cri
74 self._proc = None
75 self._devnull = open(os.devnull, 'w')
76
77 if prevent_output:
78 out = self._devnull
79 else:
80 out = sys.stderr
81
82 cri.RmRF('/tmp/cros_interface_remote_device_pid')
83 cmd_str = ' '.join(device_side_args)
84 if env:
85 env_str = ' '.join(['%s=%s' % (k, v) for k, v in env.items()])
86 cmd = env_str + ' ' + cmd_str
87 else:
88 cmd = cmd_str
89 contents = """%s&\n""" % cmd
90 contents += 'echo $! > /tmp/cros_interface_remote_device_pid\n'
91 cri.PushContents(contents, '/tmp/cros_interface_remote_device_bootstrap.sh')
92
93 cmdline = ['/bin/bash']
94 if login_shell:
95 cmdline.append('-l')
96 cmdline.append('/tmp/cros_interface_remote_device_bootstrap.sh')
97 proc = subprocess.Popen(
98 cri.FormSSHCommandLine(cmdline,
99 extra_ssh_args=extra_ssh_args),
100 stdout=out,
101 stderr=out,
102 stdin=self._devnull,
103 shell=False)
104
105 time.sleep(0.1)
106 def TryGetResult():
107 try:
108 self._pid = cri.GetFileContents(
109 '/tmp/cros_interface_remote_device_pid').strip()
110 return True
111 except OSError:
112 return False
113 try:
114 util.WaitFor(TryGetResult, 5)
115 except util.TimeoutException:
116 raise Exception('Something horrible has happened!')
117
118 # Killing the ssh session leaves the process running. We dont
119 # need it anymore, unless we have port-forwards.
120 if not leave_ssh_alive:
121 proc.kill()
122 else:
123 self._proc = proc
124
125 self._pid = int(self._pid)
126 if not self.IsAlive():
127 raise OSError('Process did not come up or did not stay alive very long!')
128 self._cri = cri
129
130 def Close(self, try_sigint_first=False):
131 if self.IsAlive():
132 # Try to politely shutdown, first.
133 if try_sigint_first:
134 logging.debug("kill -INT %i" % self._pid)
135 self._cri.GetAllCmdOutput(
136 ['kill', '-INT', str(self._pid)], quiet=True)
137 try:
138 self.Wait(timeout=0.5)
139 except util.TimeoutException:
140 pass
141
142 if self.IsAlive():
143 logging.debug("kill -KILL %i" % self._pid)
144 self._cri.GetAllCmdOutput(
145 ['kill', '-KILL', str(self._pid)], quiet=True)
146 try:
147 self.Wait(timeout=5)
148 except util.TimeoutException:
149 pass
150
151 if self.IsAlive():
152 raise Exception('Could not shutdown the process.')
153
154 self._cri = None
155 if self._proc:
156 self._proc.kill()
157 self._proc = None
158
159 if self._devnull:
160 self._devnull.close()
161 self._devnull = None
162
163 def __enter__(self):
164 return self
165
166 def __exit__(self, *args):
167 self.Close()
168 return
169
170 def Wait(self, timeout=1):
171 if not self._pid:
172 raise Exception('Closed')
173 def IsDone():
174 return not self.IsAlive()
175 util.WaitFor(IsDone, timeout)
176 self._pid = None
177
178 def IsAlive(self, quiet=True):
179 if not self._pid:
180 return False
181 exists = self._cri.FileExistsOnDevice('/proc/%i/cmdline' % self._pid,
182 quiet=quiet)
183 return exists
184
185 def HasSSH():
186 try:
187 RunCmd(['ssh'], quiet=True)
188 RunCmd(['scp'], quiet=True)
189 logging.debug("HasSSH()->True")
190 return True
191 except OSError:
192 logging.debug("HasSSH()->False")
193 return False
194
195 class LoginException(Exception):
196 pass
197
198 class KeylessLoginRequiredException(LoginException):
199 pass
200
201 class CrOSInterface(object):
202 # pylint: disable=R0923
203 def __init__(self, hostname, ssh_identity = None):
204 self._hostname = hostname
205 self._ssh_identity = None
206 self._hostfile = tempfile.NamedTemporaryFile()
207 self._hostfile.flush()
208 self._ssh_args = ['-o ConnectTimeout=5',
209 '-o StrictHostKeyChecking=no',
210 '-o KbdInteractiveAuthentication=no',
211 '-o PreferredAuthentications=publickey',
212 '-o UserKnownHostsFile=%s' % self._hostfile.name]
213
214 # List of ports generated from GetRemotePort() that may not be in use yet.
215 self._reserved_ports = []
216
217 if ssh_identity:
218 self._ssh_identity = os.path.abspath(os.path.expanduser(ssh_identity))
219
220 @property
221 def hostname(self):
222 return self._hostname
223
224 def FormSSHCommandLine(self, args, extra_ssh_args=None):
225 full_args = ['ssh',
226 '-o ForwardX11=no',
227 '-o ForwardX11Trusted=no',
228 '-n'] + self._ssh_args
229 if self._ssh_identity is not None:
230 full_args.extend(['-i', self._ssh_identity])
231 if extra_ssh_args:
232 full_args.extend(extra_ssh_args)
233 full_args.append('root@%s' % self._hostname)
234 full_args.extend(args)
235 return full_args
236
237 def GetAllCmdOutput(self, args, cwd=None, quiet=False):
238 return GetAllCmdOutput(self.FormSSHCommandLine(args), cwd, quiet=quiet)
239
240 def _RemoveSSHWarnings(self, toClean):
241 """Removes specific ssh warning lines from a string.
242
243 Args:
244 toClean: A string that may be containing multiple lines.
245
246 Returns:
247 A copy of toClean with all the Warning lines removed.
248 """
249 # Remove the Warning about connecting to a new host for the first time.
250 return re.sub('Warning: Permanently added [^\n]* to the list of known '
251 'hosts.\s\n', '', toClean)
252
253 def TryLogin(self):
254 logging.debug('TryLogin()')
255 stdout, stderr = self.GetAllCmdOutput(['echo', '$USER'], quiet=True)
256
257 # The initial login will add the host to the hosts file but will also print
258 # a warning to stderr that we need to remove.
259 stderr = self._RemoveSSHWarnings(stderr)
260 if stderr != '':
261 if 'Host key verification failed' in stderr:
262 raise LoginException(('%s host key verification failed. ' +
263 'SSH to it manually to fix connectivity.') %
264 self._hostname)
265 if 'Operation timed out' in stderr:
266 raise LoginException('Timed out while logging into %s' % self._hostname)
267 if 'UNPROTECTED PRIVATE KEY FILE!' in stderr:
268 raise LoginException('Permissions for %s are too open. To fix this,\n'
269 'chmod 600 %s' % (self._ssh_identity,
270 self._ssh_identity))
271 if 'Permission denied (publickey,keyboard-interactive)' in stderr:
272 raise KeylessLoginRequiredException(
273 'Need to set up ssh auth for %s' % self._hostname)
274 raise LoginException('While logging into %s, got %s' % (
275 self._hostname, stderr))
276 if stdout != 'root\n':
277 raise LoginException(
278 'Logged into %s, expected $USER=root, but got %s.' % (
279 self._hostname, stdout))
280
281 def FileExistsOnDevice(self, file_name, quiet=False):
282 stdout, stderr = self.GetAllCmdOutput([
283 'if', 'test', '-a', file_name, ';',
284 'then', 'echo', '1', ';',
285 'fi'
286 ], quiet=True)
287 if stderr != '':
288 if "Connection timed out" in stderr:
289 raise OSError('Machine wasn\'t responding to ssh: %s' %
290 stderr)
291 raise OSError('Unepected error: %s' % stderr)
292 exists = stdout == '1\n'
293 if not quiet:
294 logging.debug("FileExistsOnDevice(<text>, %s)->%s" % (
295 file_name, exists))
296 return exists
297
298 def PushFile(self, filename, remote_filename):
299 args = ['scp', '-r' ] + self._ssh_args
300 if self._ssh_identity:
301 args.extend(['-i', self._ssh_identity])
302
303 args.extend([os.path.abspath(filename),
304 'root@%s:%s' % (self._hostname, remote_filename)])
305
306 stdout, stderr = GetAllCmdOutput(args, quiet=True)
307 if stderr != '':
308 assert 'No such file or directory' in stderr
309 raise OSError
310
311 def PushContents(self, text, remote_filename):
312 logging.debug("PushContents(<text>, %s)" % remote_filename)
313 with tempfile.NamedTemporaryFile() as f:
314 f.write(text)
315 f.flush()
316 self.PushFile(f.name, remote_filename)
317
318 def GetFileContents(self, filename):
319 with tempfile.NamedTemporaryFile() as f:
320 args = ['scp'] + self._ssh_args
321 if self._ssh_identity:
322 args.extend(['-i', self._ssh_identity])
323
324 args.extend(['root@%s:%s' % (self._hostname, filename),
325 os.path.abspath(f.name)])
326
327 stdout, stderr = GetAllCmdOutput(args, quiet=True)
328
329 if stderr != '':
330 assert 'No such file or directory' in stderr
331 raise OSError
332
333 with open(f.name, 'r') as f2:
334 res = f2.read()
335 logging.debug("GetFileContents(%s)->%s" % (filename, res))
336 return res
337
338 def ListProcesses(self):
339 stdout, stderr = self.GetAllCmdOutput([
340 '/bin/ps', '--no-headers',
341 '-A',
342 '-o', 'pid,args'], quiet=True)
343 assert stderr == ''
344 procs = []
345 for l in stdout.split('\n'): # pylint: disable=E1103
346 if l == '':
347 continue
348 m = re.match('^\s*(\d+)\s+(.+)', l, re.DOTALL)
349 assert m
350 procs.append(m.groups())
351 logging.debug("ListProcesses(<predicate>)->[%i processes]" % len(procs))
352 return procs
353
354 def RmRF(self, filename):
355 logging.debug("rm -rf %s" % filename)
356 self.GetCmdOutput(['rm', '-rf', filename], quiet=True)
357
358 def KillAllMatching(self, predicate):
359 kills = ['kill', '-KILL']
360 for p in self.ListProcesses():
361 if predicate(p[1]):
362 logging.info('Killing %s', repr(p))
363 kills.append(p[0])
364 logging.debug("KillAllMatching(<predicate>)->%i" % (len(kills) - 2))
365 if len(kills) > 2:
366 self.GetCmdOutput(kills, quiet=True)
367 return len(kills) - 2
368
369 def IsServiceRunning(self, service_name):
370 stdout, stderr = self.GetAllCmdOutput([
371 'status', service_name], quiet=True)
372 assert stderr == ''
373 running = 'running, process' in stdout
374 logging.debug("IsServiceRunning(%s)->%s" % (service_name, running))
375 return running
376
377 def GetCmdOutput(self, args, quiet=False):
378 stdout, stderr = self.GetAllCmdOutput(args, quiet=True)
379 assert stderr == ''
380 if not quiet:
381 logging.debug("GetCmdOutput(%s)->%s" % (repr(args), stdout))
382 return stdout
383
384 def GetRemotePort(self):
385 netstat = self.GetAllCmdOutput(['netstat', '-ant'])
386 netstat = netstat[0].split('\n')
387 ports_in_use = []
388
389 for line in netstat[2:]:
390 if not line:
391 continue
392 address_in_use = line.split()[3]
393 port_in_use = address_in_use.split(':')[-1]
394 ports_in_use.append(int(port_in_use))
395
396 ports_in_use.extend(self._reserved_ports)
397
398 new_port = sorted(ports_in_use)[-1] + 1
399 self._reserved_ports.append(new_port)
400
401 return new_port
402
403 def IsHTTPServerRunningOnPort(self, port):
404 wget_output = self.GetAllCmdOutput(
405 ['wget', 'localhost:%i' % (port), '-T1', '-t1'])
406
407 if 'Connection refused' in wget_output[1]:
408 return False
409
410 return True
OLDNEW
« no previous file with comments | « tools/telemetry/telemetry/cros_browser_finder_unittest.py ('k') | tools/telemetry/telemetry/cros_interface_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698