Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / lib / remote_access.py
1 # Copyright (c) 2012 The Chromium OS 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
5 """Library containing functions to access a remote test device."""
6
7 from __future__ import print_function
8
9 import glob
10 import logging
11 import os
12 import shutil
13 import socket
14 import stat
15 import tempfile
16 import time
17
18 from chromite.lib import cros_build_lib
19 from chromite.lib import osutils
20 from chromite.lib import timeout_util
21
22
23 _path = os.path.dirname(os.path.realpath(__file__))
24 TEST_PRIVATE_KEY = os.path.normpath(
25     os.path.join(_path, '../ssh_keys/testing_rsa'))
26 del _path
27
28 LOCALHOST = 'localhost'
29 LOCALHOST_IP = '127.0.0.1'
30 ROOT_ACCOUNT = 'root'
31
32 REBOOT_MARKER = '/tmp/awaiting_reboot'
33 REBOOT_MAX_WAIT = 120
34 REBOOT_SSH_CONNECT_TIMEOUT = 2
35 REBOOT_SSH_CONNECT_ATTEMPTS = 2
36 CHECK_INTERVAL = 5
37 DEFAULT_SSH_PORT = 22
38 SSH_ERROR_CODE = 255
39
40 # Dev/test packages are installed in these paths.
41 DEV_BIN_PATHS = '/usr/local/bin:/usr/local/sbin'
42
43
44 class SSHConnectionError(Exception):
45   """Raised when SSH connection has failed."""
46
47
48 class DeviceNotPingable(Exception):
49   """Raised when device is not pingable."""
50
51
52 def NormalizePort(port, str_ok=True):
53   """Checks if |port| is a valid port number and returns the number.
54
55   Args:
56     port: The port to normalize.
57     str_ok: Accept |port| in string. If set False, only accepts
58       an integer. Defaults to True.
59
60   Returns:
61     A port number (integer).
62   """
63   err_msg = '%s is not a valid port number.' % port
64
65   if not str_ok and not isinstance(port, int):
66     raise ValueError(err_msg)
67
68   port = int(port)
69   if port <= 0 or port >= 65536:
70     raise ValueError(err_msg)
71
72   return port
73
74
75 def GetUnusedPort(ip=LOCALHOST, family=socket.AF_INET,
76                   stype=socket.SOCK_STREAM):
77   """Returns a currently unused port.
78
79   Args:
80     ip: IP to use to bind the port.
81     family: Address family.
82     stype: Socket type.
83
84   Returns:
85     A port number (integer).
86   """
87   s = None
88   try:
89     s = socket.socket(family, stype)
90     s.bind((ip, 0))
91     return s.getsockname()[1]
92   except (socket.error, OSError):
93     if s:
94       s.close()
95
96
97 def RunCommandFuncWrapper(func, msg, *args, **kwargs):
98   """Wraps a function that invokes cros_build_lib.RunCommand.
99
100   If the command failed, logs warning |msg| if error_code_ok is set;
101   logs error |msg| if error_code_ok is not set.
102
103   Args:
104     func: The function to call.
105     msg: The message to display if the command failed.
106     *args: Arguments to pass to |func|.
107     **kwargs: Keyword arguments to pass to |func|.
108
109   Returns:
110     The result of |func|.
111
112   Raises:
113     cros_build_lib.RunCommandError if the command failed and error_code_ok
114     is not set.
115   """
116   error_code_ok = kwargs.pop('error_code_ok', False)
117   result = func(*args, error_code_ok=True, **kwargs)
118   if result.returncode != 0 and not error_code_ok:
119     raise cros_build_lib.RunCommandError(msg, result)
120
121   if result.returncode != 0:
122     logging.warning(msg)
123
124
125 def CompileSSHConnectSettings(ConnectTimeout=30, ConnectionAttempts=4):
126   return ['-o', 'ConnectTimeout=%s' % ConnectTimeout,
127           '-o', 'ConnectionAttempts=%s' % ConnectionAttempts,
128           '-o', 'NumberOfPasswordPrompts=0',
129           '-o', 'Protocol=2',
130           '-o', 'ServerAliveInterval=10',
131           '-o', 'ServerAliveCountMax=3',
132           '-o', 'StrictHostKeyChecking=no',
133           '-o', 'UserKnownHostsFile=/dev/null', ]
134
135
136 class RemoteAccess(object):
137   """Provides access to a remote test machine."""
138
139   DEFAULT_USERNAME = ROOT_ACCOUNT
140
141   def __init__(self, remote_host, tempdir, port=None, username=None,
142                private_key=None, debug_level=logging.DEBUG, interactive=True):
143     """Construct the object.
144
145     Args:
146       remote_host: The ip or hostname of the remote test machine.  The test
147                    machine should be running a ChromeOS test image.
148       tempdir: A directory that RemoteAccess can use to store temporary files.
149                It's the responsibility of the caller to remove it.
150       port: The ssh port of the test machine to connect to.
151       username: The ssh login username (default: root).
152       private_key: The identify file to pass to `ssh -i` (default: testing_rsa).
153       debug_level: Logging level to use for all RunCommand invocations.
154       interactive: If set to False, pass /dev/null into stdin for the sh cmd.
155     """
156     self.tempdir = tempdir
157     self.remote_host = remote_host
158     self.port = port if port else DEFAULT_SSH_PORT
159     self.username = username if username else self.DEFAULT_USERNAME
160     self.debug_level = debug_level
161     private_key_src = private_key if private_key else TEST_PRIVATE_KEY
162     self.private_key = os.path.join(
163         tempdir, os.path.basename(private_key_src))
164
165     self.interactive = interactive
166     shutil.copyfile(private_key_src, self.private_key)
167     os.chmod(self.private_key, stat.S_IRUSR)
168
169   @property
170   def target_ssh_url(self):
171     return '%s@%s' % (self.username, self.remote_host)
172
173   def _GetSSHCmd(self, connect_settings=None):
174     if connect_settings is None:
175       connect_settings = CompileSSHConnectSettings()
176
177     cmd = (['ssh', '-p', str(self.port)] +
178            connect_settings +
179            ['-i', self.private_key])
180     if not self.interactive:
181       cmd.append('-n')
182
183     return cmd
184
185   def RemoteSh(self, cmd, connect_settings=None, error_code_ok=False,
186                remote_sudo=False, ssh_error_ok=False, **kwargs):
187     """Run a sh command on the remote device through ssh.
188
189     Args:
190       cmd: The command string or list to run.
191       connect_settings: The SSH connect settings to use.
192       error_code_ok: Does not throw an exception when the command exits with a
193                      non-zero returncode.  This does not cover the case where
194                      the ssh command itself fails (return code 255).
195                      See ssh_error_ok.
196       ssh_error_ok: Does not throw an exception when the ssh command itself
197                     fails (return code 255).
198       remote_sudo: If set, run the command in remote shell with sudo.
199       **kwargs: See cros_build_lib.RunCommand documentation.
200
201     Returns:
202       A CommandResult object.  The returncode is the returncode of the command,
203       or 255 if ssh encountered an error (could not connect, connection
204       interrupted, etc.)
205
206     Raises:
207       RunCommandError when error is not ignored through the error_code_ok flag.
208       SSHConnectionError when ssh command error is not ignored through
209       the ssh_error_ok flag.
210
211     """
212     kwargs.setdefault('capture_output', True)
213     kwargs.setdefault('debug_level', self.debug_level)
214
215     ssh_cmd = self._GetSSHCmd(connect_settings)
216     ssh_cmd += [self.target_ssh_url, '--']
217
218     if remote_sudo and self.username != ROOT_ACCOUNT:
219       # Prepend sudo to cmd.
220       ssh_cmd.append('sudo')
221
222     if isinstance(cmd, basestring):
223       ssh_cmd += [cmd]
224     else:
225       ssh_cmd += cmd
226
227     try:
228       return cros_build_lib.RunCommand(ssh_cmd, **kwargs)
229     except cros_build_lib.RunCommandError as e:
230       if ((e.result.returncode == SSH_ERROR_CODE and ssh_error_ok) or
231           (e.result.returncode and e.result.returncode != SSH_ERROR_CODE
232            and error_code_ok)):
233         return e.result
234       elif e.result.returncode == SSH_ERROR_CODE:
235         raise SSHConnectionError(e.result.error)
236       else:
237         raise
238
239   def _CheckIfRebooted(self):
240     """Checks whether a remote device has rebooted successfully.
241
242     This uses a rapidly-retried SSH connection, which will wait for at most
243     about ten seconds. If the network returns an error (e.g. host unreachable)
244     the actual delay may be shorter.
245
246     Returns:
247       Whether the device has successfully rebooted.
248     """
249     # In tests SSH seems to be waiting rather longer than would be expected
250     # from these parameters. These values produce a ~5 second wait.
251     connect_settings = CompileSSHConnectSettings(
252         ConnectTimeout=REBOOT_SSH_CONNECT_TIMEOUT,
253         ConnectionAttempts=REBOOT_SSH_CONNECT_ATTEMPTS)
254     cmd = "[ ! -e '%s' ]" % REBOOT_MARKER
255     result = self.RemoteSh(cmd, connect_settings=connect_settings,
256                            error_code_ok=True, ssh_error_ok=True,
257                            capture_output=True)
258
259     errors = {0: 'Reboot complete.',
260               1: 'Device has not yet shutdown.',
261               255: 'Cannot connect to device; reboot in progress.'}
262     if result.returncode not in errors:
263       raise Exception('Unknown error code %s returned by %s.'
264                       % (result.returncode, cmd))
265
266     logging.info(errors[result.returncode])
267     return result.returncode == 0
268
269   def RemoteReboot(self):
270     """Reboot the remote device."""
271     logging.info('Rebooting %s...', self.remote_host)
272     if self.username != ROOT_ACCOUNT:
273       self.RemoteSh('sudo sh -c "touch %s && sudo reboot"' % REBOOT_MARKER)
274     else:
275       self.RemoteSh('touch %s && reboot' % REBOOT_MARKER)
276
277     time.sleep(CHECK_INTERVAL)
278     try:
279       timeout_util.WaitForReturnTrue(self._CheckIfRebooted, REBOOT_MAX_WAIT,
280                                      period=CHECK_INTERVAL)
281     except timeout_util.TimeoutError:
282       cros_build_lib.Die('Reboot has not completed after %s seconds; giving up.'
283                          % (REBOOT_MAX_WAIT,))
284
285   def Rsync(self, src, dest, to_local=False, follow_symlinks=False,
286             recursive=True, inplace=False, verbose=False, sudo=False,
287             remote_sudo=False, **kwargs):
288     """Rsync a path to the remote device.
289
290     Rsync a path to the remote device. If |to_local| is set True, it
291     rsyncs the path from the remote device to the local machine.
292
293     Args:
294       src: The local src directory.
295       dest: The remote dest directory.
296       to_local: If set, rsync remote path to local path.
297       follow_symlinks: If set, transform symlinks into referent
298         path. Otherwise, copy symlinks as symlinks.
299       recursive: Whether to recursively copy entire directories.
300       inplace: If set, cause rsync to overwrite the dest files in place.  This
301         conserves space, but has some side effects - see rsync man page.
302       verbose: If set, print more verbose output during rsync file transfer.
303       sudo: If set, invoke the command via sudo.
304       remote_sudo: If set, run the command in remote shell with sudo.
305       **kwargs: See cros_build_lib.RunCommand documentation.
306     """
307     kwargs.setdefault('debug_level', self.debug_level)
308
309     ssh_cmd = ' '.join(self._GetSSHCmd())
310     rsync_cmd = ['rsync', '--perms', '--verbose', '--times', '--compress',
311                  '--omit-dir-times', '--exclude', '.svn']
312     rsync_cmd.append('--copy-links' if follow_symlinks else '--links')
313     rsync_sudo = 'sudo' if (
314         remote_sudo and self.username != ROOT_ACCOUNT) else ''
315     rsync_cmd += ['--rsync-path',
316                   'PATH=%s:$PATH %s rsync' % (DEV_BIN_PATHS, rsync_sudo)]
317
318     if verbose:
319       rsync_cmd.append('--progress')
320     if recursive:
321       rsync_cmd.append('--recursive')
322     if inplace:
323       rsync_cmd.append('--inplace')
324
325     if to_local:
326       rsync_cmd += ['--rsh', ssh_cmd,
327                     '[%s]:%s' % (self.target_ssh_url, src), dest]
328     else:
329       rsync_cmd += ['--rsh', ssh_cmd, src,
330                     '[%s]:%s' % (self.target_ssh_url, dest)]
331
332     rc_func = cros_build_lib.RunCommand
333     if sudo:
334       rc_func = cros_build_lib.SudoRunCommand
335     return rc_func(rsync_cmd, print_cmd=verbose, **kwargs)
336
337   def RsyncToLocal(self, *args, **kwargs):
338     """Rsync a path from the remote device to the local machine."""
339     return self.Rsync(*args, to_local=kwargs.pop('to_local', True), **kwargs)
340
341   def Scp(self, src, dest, to_local=False, recursive=True, verbose=False,
342           sudo=False, **kwargs):
343     """Scp a file or directory to the remote device.
344
345     Args:
346       src: The local src file or directory.
347       dest: The remote dest location.
348       to_local: If set, scp remote path to local path.
349       recursive: Whether to recursively copy entire directories.
350       verbose: If set, print more verbose output during scp file transfer.
351       sudo: If set, invoke the command via sudo.
352       remote_sudo: If set, run the command in remote shell with sudo.
353       **kwargs: See cros_build_lib.RunCommand documentation.
354
355     Returns:
356       A CommandResult object containing the information and return code of
357       the scp command.
358     """
359     remote_sudo = kwargs.pop('remote_sudo', False)
360     if remote_sudo and self.username != ROOT_ACCOUNT:
361       # TODO: Implement scp with remote sudo.
362       raise NotImplementedError('Cannot run scp with sudo!')
363
364     kwargs.setdefault('debug_level', self.debug_level)
365     # scp relies on 'scp' being in the $PATH of the non-interactive,
366     # SSH login shell.
367     scp_cmd = (['scp', '-P', str(self.port)] +
368                CompileSSHConnectSettings(ConnectTimeout=60) +
369                ['-i', self.private_key])
370
371     if not self.interactive:
372       scp_cmd.append('-n')
373
374     if recursive:
375       scp_cmd.append('-r')
376     if verbose:
377       scp_cmd.append('-v')
378
379     if to_local:
380       scp_cmd += ['%s:%s' % (self.target_ssh_url, src), dest]
381     else:
382       scp_cmd += glob.glob(src) + ['%s:%s' % (self.target_ssh_url, dest)]
383
384     rc_func = cros_build_lib.RunCommand
385     if sudo:
386       rc_func = cros_build_lib.SudoRunCommand
387
388     return rc_func(scp_cmd, print_cmd=verbose, **kwargs)
389
390   def ScpToLocal(self, *args, **kwargs):
391     """Scp a path from the remote device to the local machine."""
392     return self.Scp(*args, to_local=kwargs.pop('to_local', True), **kwargs)
393
394   def PipeToRemoteSh(self, producer_cmd, cmd, **kwargs):
395     """Run a local command and pipe it to a remote sh command over ssh.
396
397     Args:
398       producer_cmd: Command to run locally with its results piped to |cmd|.
399       cmd: Command to run on the remote device.
400       **kwargs: See RemoteSh for documentation.
401     """
402     result = cros_build_lib.RunCommand(producer_cmd, stdout_to_pipe=True,
403                                        print_cmd=False, capture_output=True)
404     return self.RemoteSh(cmd, input=kwargs.pop('input', result.output),
405                          **kwargs)
406
407
408 class RemoteDeviceHandler(object):
409   """A wrapper of RemoteDevice."""
410
411   def __init__(self, *args, **kwargs):
412     """Creates a RemoteDevice object."""
413     self.device = RemoteDevice(*args, **kwargs)
414
415   def __enter__(self):
416     """Return the temporary directory."""
417     return self.device
418
419   def __exit__(self, _type, _value, _traceback):
420     """Cleans up the device."""
421     self.device.Cleanup()
422
423
424 class ChromiumOSDeviceHandler(object):
425   """A wrapper of ChromiumOSDevice."""
426
427   def __init__(self, *args, **kwargs):
428     """Creates a RemoteDevice object."""
429     self.device = ChromiumOSDevice(*args, **kwargs)
430
431   def __enter__(self):
432     """Return the temporary directory."""
433     return self.device
434
435   def __exit__(self, _type, _value, _traceback):
436     """Cleans up the device."""
437     self.device.Cleanup()
438
439
440 class RemoteDevice(object):
441   """Handling basic SSH communication with a remote device."""
442
443   DEFAULT_BASE_DIR = '/tmp/remote-access'
444
445   def __init__(self, hostname, port=None, username=None,
446                base_dir=DEFAULT_BASE_DIR, connect_settings=None,
447                private_key=None, debug_level=logging.DEBUG, ping=True):
448     """Initializes a RemoteDevice object.
449
450     Args:
451       hostname: The hostname of the device.
452       port: The ssh port of the device.
453       username: The ssh login username.
454       base_dir: The base directory of the working directory on the device.
455       connect_settings: Default SSH connection settings.
456       private_key: The identify file to pass to `ssh -i`.
457       debug_level: Setting debug level for logging.
458       ping: Whether to ping the device before attempting to connect.
459     """
460     self.hostname = hostname
461     self.port = port
462     self.username = username
463     # The tempdir is for storing the rsa key and/or some temp files.
464     self.tempdir = osutils.TempDir(prefix='ssh-tmp')
465     self.connect_settings = (connect_settings if connect_settings else
466                              CompileSSHConnectSettings())
467     self.private_key = private_key
468     self.agent = self._SetupSSH()
469     self.debug_level = debug_level
470     # Setup a working directory on the device.
471     self.base_dir = base_dir
472
473     if ping and not self.Pingable():
474       raise DeviceNotPingable('Device %s is not pingable.' % self.hostname)
475
476     # Do not call RunCommand here because we have not set up work directory yet.
477     self.BaseRunCommand(['mkdir', '-p', self.base_dir])
478     self.work_dir = self.BaseRunCommand(
479         ['mktemp', '-d', '--tmpdir=%s' % base_dir],
480         capture_output=True).output.strip()
481     logging.debug(
482         'The tempory working directory on the device is %s', self.work_dir)
483
484     self.cleanup_cmds = []
485     self.RegisterCleanupCmd(['rm', '-rf', self.work_dir])
486
487   def Pingable(self, timeout=20):
488     """Returns True if the device is pingable.
489
490     Args:
491       timeout: Timeout in seconds (default: 20 seconds).
492
493     Returns:
494       True if the device responded to the ping before |timeout|.
495     """
496     result = cros_build_lib.RunCommand(
497         ['ping', '-c', '1', '-w', str(timeout), self.hostname],
498         error_code_ok=True,
499         capture_output=True)
500     return result.returncode == 0
501
502   def _SetupSSH(self):
503     """Setup the ssh connection with device."""
504     return RemoteAccess(self.hostname, self.tempdir.tempdir, port=self.port,
505                         username=self.username, private_key=self.private_key)
506
507   def _HasRsync(self):
508     """Checks if rsync exists on the device."""
509     result = self.agent.RemoteSh(['PATH=%s:$PATH rsync' % DEV_BIN_PATHS,
510                                   '--version'], error_code_ok=True)
511     return result.returncode == 0
512
513   def RegisterCleanupCmd(self, cmd, **kwargs):
514     """Register a cleanup command to be run on the device in Cleanup().
515
516     Args:
517       cmd: command to run. See RemoteAccess.RemoteSh documentation.
518       **kwargs: keyword arguments to pass along with cmd. See
519         RemoteAccess.RemoteSh documentation.
520     """
521     self.cleanup_cmds.append((cmd, kwargs))
522
523   def Cleanup(self):
524     """Remove work/temp directories and run all registered cleanup commands."""
525     for cmd, kwargs in self.cleanup_cmds:
526       # We want to run through all cleanup commands even if there are errors.
527       kwargs.setdefault('error_code_ok', True)
528       self.BaseRunCommand(cmd, **kwargs)
529
530     self.tempdir.Cleanup()
531
532   def CopyToDevice(self, src, dest, mode=None, **kwargs):
533     """Copy path to device."""
534     msg = 'Could not copy %s to device.' % src
535     if mode is None:
536       # Use rsync by default if it exists.
537       mode = 'rsync' if self._HasRsync() else 'scp'
538
539     if mode == 'scp':
540       # scp always follow symlinks
541       kwargs.pop('follow_symlinks', None)
542       func  = self.agent.Scp
543     else:
544       func = self.agent.Rsync
545
546     return RunCommandFuncWrapper(func, msg, src, dest, **kwargs)
547
548   def CopyFromDevice(self, src, dest, mode=None, **kwargs):
549     """Copy path from device."""
550     msg = 'Could not copy %s from device.' % src
551     if mode is None:
552       # Use rsync by default if it exists.
553       mode = 'rsync' if self._HasRsync() else 'scp'
554
555     if mode == 'scp':
556       # scp always follow symlinks
557       kwargs.pop('follow_symlinks', None)
558       func  = self.agent.ScpToLocal
559     else:
560       func = self.agent.RsyncToLocal
561
562     return RunCommandFuncWrapper(func, msg, src, dest, **kwargs)
563
564   def CopyFromWorkDir(self, src, dest, **kwargs):
565     """Copy path from working directory on the device."""
566     return self.CopyFromDevice(os.path.join(self.work_dir, src), dest, **kwargs)
567
568   def CopyToWorkDir(self, src, dest='', **kwargs):
569     """Copy path to working directory on the device."""
570     return self.CopyToDevice(src, os.path.join(self.work_dir, dest), **kwargs)
571
572   def PipeOverSSH(self, filepath, cmd, **kwargs):
573     """Cat a file and pipe over SSH."""
574     producer_cmd = ['cat', filepath]
575     return self.agent.PipeToRemoteSh(producer_cmd, cmd, **kwargs)
576
577   def Reboot(self):
578     """Reboot the device."""
579     return self.agent.RemoteReboot()
580
581   def BaseRunCommand(self, cmd, **kwargs):
582     """Executes a shell command on the device with output captured by default.
583
584     Args:
585       cmd: command to run. See RemoteAccess.RemoteSh documentation.
586       **kwargs: keyword arguments to pass along with cmd. See
587         RemoteAccess.RemoteSh documentation.
588     """
589     kwargs.setdefault('debug_level', self.debug_level)
590     kwargs.setdefault('connect_settings', self.connect_settings)
591     try:
592       return self.agent.RemoteSh(cmd, **kwargs)
593     except SSHConnectionError:
594       logging.error('Error connecting to device %s', self.hostname)
595       raise
596
597   def RunCommand(self, cmd, **kwargs):
598     """Executes a shell command on the device with output captured by default.
599
600     Also sets environment variables using dictionary provided by
601     keyword argument |extra_env|.
602
603     Args:
604       cmd: command to run. See RemoteAccess.RemoteSh documentation.
605       **kwargs: keyword arguments to pass along with cmd. See
606         RemoteAccess.RemoteSh documentation.
607     """
608     new_cmd = cmd
609     # Handle setting environment variables on the device by copying
610     # and sourcing a temporary environment file.
611     extra_env = kwargs.pop('extra_env', None)
612     if extra_env:
613       env_list = ['export %s=%s' % (k, cros_build_lib.ShellQuote(v))
614                   for k, v in extra_env.iteritems()]
615       remote_sudo = kwargs.pop('remote_sudo', False)
616       with tempfile.NamedTemporaryFile(dir=self.tempdir.tempdir,
617                                        prefix='env') as f:
618         logging.debug('Environment variables: %s', ' '.join(env_list))
619         osutils.WriteFile(f.name, '\n'.join(env_list))
620         self.CopyToWorkDir(f.name)
621         env_file = os.path.join(self.work_dir, os.path.basename(f.name))
622         new_cmd = ['.', '%s;' % env_file]
623         if remote_sudo and self.agent.username != ROOT_ACCOUNT:
624           new_cmd += ['sudo', '-E']
625
626         new_cmd += cmd
627
628     return self.BaseRunCommand(new_cmd, **kwargs)
629
630
631 class ChromiumOSDevice(RemoteDevice):
632   """Basic commands to interact with a ChromiumOS device over SSH connection."""
633
634   MAKE_DEV_SSD_BIN = '/usr/share/vboot/bin/make_dev_ssd.sh'
635   MOUNT_ROOTFS_RW_CMD = ['mount', '-o', 'remount,rw', '/']
636   LIST_MOUNTS_CMD = ['cat', '/proc/mounts']
637   GET_BOARD_CMD = ['grep', 'CHROMEOS_RELEASE_BOARD', '/etc/lsb-release']
638
639   def __init__(self, *args, **kwargs):
640     super(ChromiumOSDevice, self).__init__(*args, **kwargs)
641     self.board = self._LearnBoard()
642     self.path = self._GetPath()
643
644   def _GetPath(self):
645     """Gets $PATH on the device and prepend it with DEV_BIN_PATHS."""
646     try:
647       result = self.BaseRunCommand(['echo', "${PATH}"])
648     except cros_build_lib.RunCommandError:
649       logging.warning('Error detecting $PATH on the device.')
650       raise
651
652     return '%s:%s' % (DEV_BIN_PATHS, result.output.strip())
653
654   def _RemountRootfsAsWritable(self):
655     """Attempts to Remount the root partition."""
656     logging.info("Remounting '/' with rw...")
657     self.RunCommand(self.MOUNT_ROOTFS_RW_CMD, error_code_ok=True,
658                     remote_sudo=True)
659
660   def _RootfsIsReadOnly(self):
661     """Returns True if rootfs on is mounted as read-only."""
662     r = self.RunCommand(self.LIST_MOUNTS_CMD, capture_output=True)
663     for line in r.output.splitlines():
664       if not line:
665         continue
666
667       chunks = line.split()
668       if chunks[1] == '/' and 'ro' in chunks[3].split(','):
669         return True
670
671     return False
672
673   def DisableRootfsVerification(self):
674     """Disables device rootfs verification."""
675     logging.info('Disabling rootfs verification on device...')
676     self.RunCommand(
677         [self.MAKE_DEV_SSD_BIN, '--remove_rootfs_verification', '--force'],
678         error_code_ok=True, remote_sudo=True)
679     # TODO(yjhong): Make sure an update is not pending.
680     logging.info('Need to reboot to actually disable the verification.')
681     self.Reboot()
682
683   def MountRootfsReadWrite(self):
684     """Checks mount types and remounts them as read-write if needed.
685
686     Returns:
687       True if rootfs is mounted as read-write. False otherwise.
688     """
689     if not self._RootfsIsReadOnly():
690       return True
691
692     # If the image on the device is built with rootfs verification
693     # disabled, we can simply remount '/' as read-write.
694     self._RemountRootfsAsWritable()
695
696     if not self._RootfsIsReadOnly():
697       return True
698
699     logging.info('Unable to remount rootfs as rw (normal w/verified rootfs).')
700     # If the image is built with rootfs verification, turn off the
701     # rootfs verification. After reboot, the rootfs will be mounted as
702     # read-write (there is no need to remount).
703     self.DisableRootfsVerification()
704
705     return not self._RootfsIsReadOnly()
706
707   def _LearnBoard(self):
708     """Grab the board reported by the remote device.
709
710     In the case of multiple matches, uses the first one.  In the case of no
711     entry or if the command failed, returns an empty string.
712     """
713     try:
714       result = self.BaseRunCommand(self.GET_BOARD_CMD, capture_output=True)
715     except cros_build_lib.RunCommandError:
716       logging.warning('Error detecting the board.')
717       return ''
718
719     # In the case of multiple matches, use the first one.
720     output = result.output.splitlines()
721     if len(output) > 1:
722       logging.debug('More than one board entry found!  Using the first one.')
723
724     return output[0].strip().partition('=')[-1]
725
726   def RunCommand(self, cmd, **kwargs):
727     """Executes a shell command on the device with output captured by default.
728
729     Also makes sure $PATH is set correctly by adding DEV_BIN_PATHS to
730     'PATH' in |extra_env|.
731
732     Args:
733       cmd: command to run. See RemoteAccess.RemoteSh documentation.
734       **kwargs: keyword arguments to pass along with cmd. See
735         RemoteAccess.RemoteSh documentation.
736     """
737     extra_env = kwargs.pop('extra_env', {})
738     path_env = extra_env.get('PATH', None)
739     path_env = self.path if not path_env else '%s:%s' % (path_env, self.path)
740     extra_env['PATH'] = path_env
741     kwargs['extra_env'] = extra_env
742     return super(ChromiumOSDevice, self).RunCommand(cmd, **kwargs)