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