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

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

Issue 23072018: [telemetry] Move telemetry/core/chrome/ to telemetry/core/backends/chrome/ (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Fix perf smoothness_unittest. Created 7 years, 3 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 tempfile
11
12 # TODO(nduca): This whole file is built up around making individual ssh calls
13 # for each operation. It really could get away with a single ssh session built
14 # around pexpect, I suspect, if we wanted it to be faster. But, this was
15 # convenient.
16
17 def IsRunningOnCrosDevice():
18 """Returns True if we're on a ChromeOS device."""
19 lsb_release = '/etc/lsb-release'
20 if sys.platform.startswith('linux') and os.path.exists(lsb_release):
21 with open(lsb_release, 'r') as f:
22 res = f.read()
23 if res.count('CHROMEOS_RELEASE_NAME'):
24 return True
25 return False
26
27 def RunCmd(args, cwd=None, quiet=False):
28 """Opens a subprocess to execute a program and returns its return value.
29
30 Args:
31 args: A string or a sequence of program arguments. The program to execute is
32 the string or the first item in the args sequence.
33 cwd: If not None, the subprocess's current directory will be changed to
34 |cwd| before it's executed.
35
36 Returns:
37 Return code from the command execution.
38 """
39 if not quiet:
40 logging.debug(' '.join(args) + ' ' + (cwd or ''))
41 with open(os.devnull, 'w') as devnull:
42 p = subprocess.Popen(args=args, cwd=cwd, stdout=devnull,
43 stderr=devnull, stdin=devnull, shell=False)
44 return p.wait()
45
46 def GetAllCmdOutput(args, cwd=None, quiet=False):
47 """Open a subprocess to execute a program and returns its output.
48
49 Args:
50 args: A string or a sequence of program arguments. The program to execute is
51 the string or the first item in the args sequence.
52 cwd: If not None, the subprocess's current directory will be changed to
53 |cwd| before it's executed.
54
55 Returns:
56 Captures and returns the command's stdout.
57 Prints the command's stderr to logger (which defaults to stdout).
58 """
59 if not quiet:
60 logging.debug(' '.join(args) + ' ' + (cwd or ''))
61 with open(os.devnull, 'w') as devnull:
62 p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE,
63 stderr=subprocess.PIPE, stdin=devnull)
64 stdout, stderr = p.communicate()
65 if not quiet:
66 logging.debug(' > stdout=[%s], stderr=[%s]', stdout, stderr)
67 return stdout, stderr
68
69 def HasSSH():
70 try:
71 RunCmd(['ssh'], quiet=True)
72 RunCmd(['scp'], quiet=True)
73 logging.debug("HasSSH()->True")
74 return True
75 except OSError:
76 logging.debug("HasSSH()->False")
77 return False
78
79 class LoginException(Exception):
80 pass
81
82 class KeylessLoginRequiredException(LoginException):
83 pass
84
85 class CrOSInterface(object):
86 # pylint: disable=R0923
87 def __init__(self, hostname = None, ssh_identity = None):
88 self._hostname = hostname
89 # List of ports generated from GetRemotePort() that may not be in use yet.
90 self._reserved_ports = []
91
92 if self.local:
93 return
94
95 self._ssh_identity = None
96 self._hostfile = tempfile.NamedTemporaryFile()
97 self._hostfile.flush()
98 self._ssh_args = ['-o ConnectTimeout=5',
99 '-o StrictHostKeyChecking=no',
100 '-o KbdInteractiveAuthentication=no',
101 '-o PreferredAuthentications=publickey',
102 '-o UserKnownHostsFile=%s' % self._hostfile.name]
103
104 if ssh_identity:
105 self._ssh_identity = os.path.abspath(os.path.expanduser(ssh_identity))
106
107 @property
108 def local(self):
109 return not self._hostname
110
111 @property
112 def hostname(self):
113 return self._hostname
114
115 def FormSSHCommandLine(self, args, extra_ssh_args=None):
116 if self.local:
117 # We run the command through the shell locally for consistency with
118 # how commands are run through SSH (crbug.com/239161). This work
119 # around will be unnecessary once we implement a persistent SSH
120 # connection to run remote commands (crbug.com/239607).
121 return ['sh', '-c', " ".join(args)]
122
123 full_args = ['ssh',
124 '-o ForwardX11=no',
125 '-o ForwardX11Trusted=no',
126 '-n'] + self._ssh_args
127 if self._ssh_identity is not None:
128 full_args.extend(['-i', self._ssh_identity])
129 if extra_ssh_args:
130 full_args.extend(extra_ssh_args)
131 full_args.append('root@%s' % self._hostname)
132 full_args.extend(args)
133 return full_args
134
135 def _RemoveSSHWarnings(self, toClean):
136 """Removes specific ssh warning lines from a string.
137
138 Args:
139 toClean: A string that may be containing multiple lines.
140
141 Returns:
142 A copy of toClean with all the Warning lines removed.
143 """
144 # Remove the Warning about connecting to a new host for the first time.
145 return re.sub('Warning: Permanently added [^\n]* to the list of known '
146 'hosts.\s\n', '', toClean)
147
148 def RunCmdOnDevice(self, args, cwd=None, quiet=False):
149 stdout, stderr = GetAllCmdOutput(
150 self.FormSSHCommandLine(args), cwd, quiet=quiet)
151 # The initial login will add the host to the hosts file but will also print
152 # a warning to stderr that we need to remove.
153 stderr = self._RemoveSSHWarnings(stderr)
154 return stdout, stderr
155
156 def TryLogin(self):
157 logging.debug('TryLogin()')
158 assert not self.local
159 stdout, stderr = self.RunCmdOnDevice(['echo', '$USER'], quiet=True)
160 if stderr != '':
161 if 'Host key verification failed' in stderr:
162 raise LoginException(('%s host key verification failed. ' +
163 'SSH to it manually to fix connectivity.') %
164 self._hostname)
165 if 'Operation timed out' in stderr:
166 raise LoginException('Timed out while logging into %s' % self._hostname)
167 if 'UNPROTECTED PRIVATE KEY FILE!' in stderr:
168 raise LoginException('Permissions for %s are too open. To fix this,\n'
169 'chmod 600 %s' % (self._ssh_identity,
170 self._ssh_identity))
171 if 'Permission denied (publickey,keyboard-interactive)' in stderr:
172 raise KeylessLoginRequiredException(
173 'Need to set up ssh auth for %s' % self._hostname)
174 raise LoginException('While logging into %s, got %s' % (
175 self._hostname, stderr))
176 if stdout != 'root\n':
177 raise LoginException(
178 'Logged into %s, expected $USER=root, but got %s.' % (
179 self._hostname, stdout))
180
181 def FileExistsOnDevice(self, file_name):
182 if self.local:
183 return os.path.exists(file_name)
184
185 stdout, stderr = self.RunCmdOnDevice([
186 'if', 'test', '-e', file_name, ';',
187 'then', 'echo', '1', ';',
188 'fi'
189 ], quiet=True)
190 if stderr != '':
191 if "Connection timed out" in stderr:
192 raise OSError('Machine wasn\'t responding to ssh: %s' %
193 stderr)
194 raise OSError('Unepected error: %s' % stderr)
195 exists = stdout == '1\n'
196 logging.debug("FileExistsOnDevice(<text>, %s)->%s" % (file_name, exists))
197 return exists
198
199 def PushFile(self, filename, remote_filename):
200 if self.local:
201 args = ['cp', '-r', filename, remote_filename]
202 stdout, stderr = GetAllCmdOutput(args, quiet=True)
203 if stderr != '':
204 raise OSError('No such file or directory %s' % stderr)
205 return
206
207 args = ['scp', '-r' ] + self._ssh_args
208 if self._ssh_identity:
209 args.extend(['-i', self._ssh_identity])
210
211 args.extend([os.path.abspath(filename),
212 'root@%s:%s' % (self._hostname, remote_filename)])
213
214 stdout, stderr = GetAllCmdOutput(args, quiet=True)
215 stderr = self._RemoveSSHWarnings(stderr)
216 if stderr != '':
217 raise OSError('No such file or directory %s' % stderr)
218
219 def PushContents(self, text, remote_filename):
220 logging.debug("PushContents(<text>, %s)" % remote_filename)
221 with tempfile.NamedTemporaryFile() as f:
222 f.write(text)
223 f.flush()
224 self.PushFile(f.name, remote_filename)
225
226 def GetFileContents(self, filename):
227 assert not self.local
228 with tempfile.NamedTemporaryFile() as f:
229 args = ['scp'] + self._ssh_args
230 if self._ssh_identity:
231 args.extend(['-i', self._ssh_identity])
232
233 args.extend(['root@%s:%s' % (self._hostname, filename),
234 os.path.abspath(f.name)])
235
236 stdout, stderr = GetAllCmdOutput(args, quiet=True)
237 stderr = self._RemoveSSHWarnings(stderr)
238
239 if stderr != '':
240 raise OSError('No such file or directory %s' % stderr)
241
242 with open(f.name, 'r') as f2:
243 res = f2.read()
244 logging.debug("GetFileContents(%s)->%s" % (filename, res))
245 return res
246
247 def ListProcesses(self):
248 """Returns (pid, cmd, ppid, state) of all processes on the device."""
249 stdout, stderr = self.RunCmdOnDevice([
250 '/bin/ps', '--no-headers',
251 '-A',
252 '-o', 'pid,ppid,args,state'], quiet=True)
253 assert stderr == '', stderr
254 procs = []
255 for l in stdout.split('\n'): # pylint: disable=E1103
256 if l == '':
257 continue
258 m = re.match('^\s*(\d+)\s+(\d+)\s+(.+)\s+(.+)', l, re.DOTALL)
259 assert m
260 procs.append((int(m.group(1)), m.group(3), int(m.group(2)), m.group(4)))
261 logging.debug("ListProcesses(<predicate>)->[%i processes]" % len(procs))
262 return procs
263
264 def RmRF(self, filename):
265 logging.debug("rm -rf %s" % filename)
266 self.RunCmdOnDevice(['rm', '-rf', filename], quiet=True)
267
268 def KillAllMatching(self, predicate):
269 kills = ['kill', '-KILL']
270 for pid, cmd, _, _ in self.ListProcesses():
271 if predicate(cmd):
272 logging.info('Killing %s, pid %d' % cmd, pid)
273 kills.append(pid)
274 logging.debug("KillAllMatching(<predicate>)->%i" % (len(kills) - 2))
275 if len(kills) > 2:
276 self.RunCmdOnDevice(kills, quiet=True)
277 return len(kills) - 2
278
279 def IsServiceRunning(self, service_name):
280 stdout, stderr = self.RunCmdOnDevice([
281 'status', service_name], quiet=True)
282 assert stderr == '', stderr
283 running = 'running, process' in stdout
284 logging.debug("IsServiceRunning(%s)->%s" % (service_name, running))
285 return running
286
287 def GetRemotePort(self):
288 netstat = self.RunCmdOnDevice(['netstat', '-ant'])
289 netstat = netstat[0].split('\n')
290 ports_in_use = []
291
292 for line in netstat[2:]:
293 if not line:
294 continue
295 address_in_use = line.split()[3]
296 port_in_use = address_in_use.split(':')[-1]
297 ports_in_use.append(int(port_in_use))
298
299 ports_in_use.extend(self._reserved_ports)
300
301 new_port = sorted(ports_in_use)[-1] + 1
302 self._reserved_ports.append(new_port)
303
304 return new_port
305
306 def IsHTTPServerRunningOnPort(self, port):
307 wget_output = self.RunCmdOnDevice(
308 ['wget', 'localhost:%i' % (port), '-T1', '-t1'])
309
310 if 'Connection refused' in wget_output[1]:
311 return False
312
313 return True
314
315 def FilesystemMountedAt(self, path):
316 """Returns the filesystem mounted at |path|"""
317 df_out, _ = self.RunCmdOnDevice(['/bin/df', path])
318 df_ary = df_out.split('\n')
319 # 3 lines for title, mount info, and empty line.
320 if len(df_ary) == 3:
321 line_ary = df_ary[1].split()
322 if line_ary:
323 return line_ary[0]
324 return None
325
326 def TakeScreenShot(self, screenshot_prefix):
327 """Takes a screenshot, useful for debugging failures."""
328 # TODO(achuith): Find a better location for screenshots. Cros autotests
329 # upload everything in /var/log so use /var/log/screenshots for now.
330 SCREENSHOT_DIR = '/var/log/screenshots/'
331 SCREENSHOT_EXT = '.png'
332
333 self.RunCmdOnDevice(['mkdir', '-p', SCREENSHOT_DIR])
334 for i in xrange(25):
335 screenshot_file = ('%s%s-%d%s' %
336 (SCREENSHOT_DIR, screenshot_prefix, i, SCREENSHOT_EXT))
337 if not self.FileExistsOnDevice(screenshot_file):
338 self.RunCmdOnDevice([
339 'DISPLAY=:0.0 XAUTHORITY=/home/chronos/.Xauthority '
340 '/usr/local/bin/import',
341 '-window root',
342 '-depth 8',
343 screenshot_file])
344 return
345 logging.warning('screenshot directory full.')
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698