Home | History | Annotate | Download | only in cros
      1 # Copyright 2014 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 import logging
      6 import os
      7 
      8 import common
      9 from autotest_lib.client.common_lib import error
     10 
     11 """
     12 Functions to query and control debugd dev tools.
     13 
     14 This file provides a set of functions to check the general state of the
     15 debugd dev tools, and a set of classes to interface to the individual
     16 tools.
     17 
     18 Current tool classes are:
     19     RootfsVerificationTool
     20     BootFromUsbTool
     21     SshServerTool
     22     SystemPasswordTool
     23 These classes have functions to check the state and enable/disable the
     24 tool. Some tools may not be able to disable themselves, in which case
     25 an exception will be thrown (for example, RootfsVerificationTool cannot
     26 be disabled).
     27 
     28 General usage will look something like this:
     29 
     30 # Make sure tools are accessible on the system.
     31 if debugd_dev_tools.are_dev_tools_available(host):
     32     # Create the tool(s) you want to interact with.
     33     tools = [debugd_dev_tools.SshServerTool(), ...]
     34     for tool in tools:
     35         # Initialize tools and save current state.
     36         tool.initialize(host, save_initial_state=True)
     37         # Perform required action with tools.
     38         tool.enable()
     39         # Restore initial tool state.
     40         tool.restore_state()
     41     # Clean up temporary files.
     42     debugd_dev_tools.remove_temp_files()
     43 """
     44 
     45 
     46 # Defined in system_api/dbus/service_constants.h.
     47 DEV_FEATURES_DISABLED = 1 << 0
     48 DEV_FEATURE_ROOTFS_VERIFICATION_REMOVED = 1 << 1
     49 DEV_FEATURE_BOOT_FROM_USB_ENABLED = 1 << 2
     50 DEV_FEATURE_SSH_SERVER_CONFIGURED = 1 << 3
     51 DEV_FEATURE_DEV_MODE_ROOT_PASSWORD_SET = 1 << 4
     52 DEV_FEATURE_SYSTEM_ROOT_PASSWORD_SET = 1 << 5
     53 
     54 
     55 # Location to save temporary files to store and load state. This folder should
     56 # be persistent through a power cycle so we can't use /tmp.
     57 _TEMP_DIR = '/usr/local/autotest/tmp/debugd_dev_tools'
     58 
     59 
     60 class AccessError(error.CmdError):
     61     """Raised when debugd D-Bus access fails."""
     62     pass
     63 
     64 
     65 class FeatureUnavailableError(error.TestNAError):
     66     """Raised when a feature cannot be enabled or disabled."""
     67     pass
     68 
     69 
     70 def query_dev_tools_state(host):
     71     """
     72     Queries debugd for the current dev features state.
     73 
     74     @param host: Host device.
     75 
     76     @return: Integer debugd query return value.
     77 
     78     @raise AccessError: Can't talk to debugd on the host.
     79     """
     80     result = _send_debugd_command(host, 'QueryDevFeatures')
     81     state = int(result.stdout)
     82     logging.debug('query_dev_tools_state = %d (0x%04X)', state, state)
     83     return state
     84 
     85 
     86 def are_dev_tools_available(host):
     87     """
     88     Check if dev tools are available on the host.
     89 
     90     @param host: Host device.
     91 
     92     @return: True if tools are available, False otherwise.
     93     """
     94     try:
     95         return query_dev_tools_state(host) != DEV_FEATURES_DISABLED
     96     except AccessError:
     97         return False
     98 
     99 
    100 def remove_temp_files(host):
    101     """
    102     Removes all DevTools temporary files and directories.
    103 
    104     Any test using dev tools should try to call this just before
    105     exiting to erase any temporary files that may have been saved.
    106 
    107     @param host: Host device.
    108     """
    109     host.run('rm -rf "%s"' % _TEMP_DIR)
    110 
    111 
    112 def expect_access_failure(host, tools):
    113     """
    114     Verifies that access is denied to all provided tools.
    115 
    116     Will check are_dev_tools_available() first to try to avoid changing
    117     device state in case access is allowed. Otherwise, the function
    118     will try to enable each tool in the list and throw an exception if
    119     any succeeds.
    120 
    121     @param host: Host device.
    122     @param tools: List of tools to checks.
    123 
    124     @raise TestFail: are_dev_tools_available() returned True or
    125                      a tool successfully enabled.
    126     """
    127     if are_dev_tools_available(host):
    128         raise error.TestFail('Unexpected dev tool access success')
    129     for tool in tools:
    130         try:
    131             tool.enable()
    132         except AccessError:
    133             # We want an exception, otherwise the tool succeeded.
    134             pass
    135         else:
    136             raise error.TestFail('Unexpected %s enable success.' % tool)
    137 
    138 
    139 def _send_debugd_command(host, name, args=()):
    140     """
    141     Sends a debugd command.
    142 
    143     @param host: Host to run the command on.
    144     @param name: String debugd D-Bus function name.
    145     @param args: List of string arguments to pass to dbus-send.
    146 
    147     @return: The dbus-send CmdResult object.
    148 
    149     @raise AccessError: debugd call returned an error.
    150     """
    151     command = ('dbus-send --system --fixed --print-reply '
    152                '--dest=org.chromium.debugd /org/chromium/debugd '
    153                '"org.chromium.debugd.%s"' % name)
    154     for arg in args:
    155         command += ' %s' % arg
    156     try:
    157         return host.run(command)
    158     except error.CmdError as e:
    159         raise AccessError(e.command, e.result_obj, e.additional_text)
    160 
    161 
    162 class DevTool(object):
    163     """
    164     Parent tool class.
    165 
    166     Each dev tool has its own child class that handles the details
    167     of disabling, enabling, and querying the functionality. This class
    168     provides some common functionality needed by multiple tools.
    169 
    170     Child classes should implement the following:
    171       - is_enabled(): use debugd to query whether the tool is enabled.
    172       - enable(): use debugd to enable the tool.
    173       - disable(): manually disable the tool.
    174       - save_state(): record the current tool state on the host.
    175       - restore_state(): restore the saved tool state.
    176 
    177     If a child class cannot perform the required action (for
    178     example the rootfs tool can't currently restore its initial
    179     state), leave the function unimplemented so it will throw an
    180     exception if a test attempts to use it.
    181     """
    182 
    183 
    184     def initialize(self, host, save_initial_state=False):
    185         """
    186         Sets up the initial tool state. This must be called on
    187         every tool before use.
    188 
    189         @param host: Device host the test is running on.
    190         @param save_initial_state: True to save the device state.
    191         """
    192         self._host = host
    193         if save_initial_state:
    194             self.save_state()
    195 
    196 
    197     def is_enabled(self):
    198         """
    199         Each tool should override this to query itself using debugd.
    200         Normally this can be done by using the provided
    201         _check_enabled() function.
    202         """
    203         self._unimplemented_function_error('is_enabled')
    204 
    205 
    206     def enable(self):
    207         """
    208         Each tool should override this to enable itself using debugd.
    209         """
    210         self._unimplemented_function_error('enable')
    211 
    212 
    213     def disable(self):
    214         """
    215         Each tool should override this to disable itself.
    216         """
    217         self._unimplemented_function_error('disable')
    218 
    219 
    220     def save_state(self):
    221         """
    222         Save the initial tool state. Should be overridden by child
    223         tool classes.
    224         """
    225         self._unimplemented_function_error('_save_state')
    226 
    227 
    228     def restore_state(self):
    229         """
    230         Restore the initial tool state. Should be overridden by child
    231         tool classes.
    232         """
    233         self._unimplemented_function_error('_restore_state')
    234 
    235 
    236     def _check_enabled(self, bits):
    237         """
    238         Checks if the given feature is currently enabled according to
    239         the debugd status query function.
    240 
    241         @param bits: Integer status bits corresponding to the features.
    242 
    243         @return: True if the status query is enabled and the
    244                  indicated bits are all set, False otherwise.
    245         """
    246         state = query_dev_tools_state(self._host)
    247         enabled = bool((state != DEV_FEATURES_DISABLED) and
    248                        (state & bits == bits))
    249         logging.debug('%s _check_enabled = %s (0x%04X / 0x%04X)',
    250                       self, enabled, state, bits)
    251         return enabled
    252 
    253 
    254     def _get_temp_path(self, source_path):
    255         """
    256         Get temporary storage path for a file or directory.
    257 
    258         Temporary path is based on the tool class name and the
    259         source directory to keep tool files isolated and prevent
    260         name conflicts within tools.
    261 
    262         The function returns a full temporary path corresponding to
    263         |source_path|.
    264 
    265         For example, _get_temp_path('/foo/bar.txt') would return
    266         '/path/to/temp/folder/debugd_dev_tools/FooTool/foo/bar.txt'.
    267 
    268         @param source_path: String path to the file or directory.
    269 
    270         @return: Temp path string.
    271         """
    272         return '%s/%s/%s' % (_TEMP_DIR, self, source_path)
    273 
    274 
    275     def _save_files(self, paths):
    276         """
    277         Saves a set of files to a temporary location.
    278 
    279         This can be used to save specific files so that a tool can
    280         save its current state before starting a test.
    281 
    282         See _restore_files() for restoring the saved files.
    283 
    284         @param paths: List of string paths to save.
    285         """
    286         for path in paths:
    287             temp_path = self._get_temp_path(path)
    288             self._host.run('mkdir -p "%s"' % os.path.dirname(temp_path))
    289             self._host.run('cp -r "%s" "%s"' % (path, temp_path),
    290                            ignore_status=True)
    291 
    292 
    293     def _restore_files(self, paths):
    294         """
    295         Restores saved files to their original location.
    296 
    297         Used to restore files that have previously been saved by
    298         _save_files(), usually to return the device to its initial
    299         state.
    300 
    301         This function does not erase the saved files, so it can
    302         be used multiple times if needed.
    303 
    304         @param paths: List of string paths to restore.
    305         """
    306         for path in paths:
    307             self._host.run('rm -rf "%s"' % path)
    308             self._host.run('cp -r "%s" "%s"' % (self._get_temp_path(path),
    309                                                 path),
    310                            ignore_status=True)
    311 
    312 
    313     def _unimplemented_function_error(self, function_name):
    314         """
    315         Throws an exception if a required tool function hasn't been
    316         implemented.
    317         """
    318         raise FeatureUnavailableError('%s has not implemented %s()' %
    319                                       (self, function_name))
    320 
    321 
    322     def __str__(self):
    323         """
    324         Tool name accessor for temporary files and logging.
    325 
    326         Based on class rather than unique instance naming since all
    327         instances of the same tool have identical functionality.
    328         """
    329         return type(self).__name__
    330 
    331 
    332 class RootfsVerificationTool(DevTool):
    333     """
    334     Rootfs verification removal tool.
    335 
    336     This tool is currently unable to transition from non-verified back
    337     to verified rootfs; it may potentially require re-flashing an OS.
    338     Since devices in the test lab run in verified mode, this tool is
    339     unsuitable for automated testing until this capability is
    340     implemented.
    341     """
    342 
    343 
    344     def is_enabled(self):
    345         return self._check_enabled(DEV_FEATURE_ROOTFS_VERIFICATION_REMOVED)
    346 
    347 
    348     def enable(self):
    349         _send_debugd_command(self._host, 'RemoveRootfsVerification')
    350         self._host.reboot()
    351 
    352 
    353     def disable(self):
    354         raise FeatureUnavailableError('Cannot re-enable rootfs verification')
    355 
    356 
    357 class BootFromUsbTool(DevTool):
    358     """
    359     USB boot configuration tool.
    360 
    361     Certain boards have restrictions with USB booting. Mario can't
    362     boot from USB at all, and Alex/ZGB can't disable USB booting
    363     once it's been enabled. Any attempts to perform these operation
    364     will raise a FeatureUnavailableError exception.
    365     """
    366 
    367 
    368     # Lists of which platforms can't enable or disable USB booting.
    369     ENABLE_UNAVAILABLE_PLATFORMS = ('mario',)
    370     DISABLE_UNAVAILABLE_PLATFORMS = ('mario', 'alex', 'zgb')
    371 
    372 
    373     def is_enabled(self):
    374         return self._check_enabled(DEV_FEATURE_BOOT_FROM_USB_ENABLED)
    375 
    376 
    377     def enable(self):
    378         platform = self._host.get_platform().lower()
    379         if any(p in platform for p in self.ENABLE_UNAVAILABLE_PLATFORMS):
    380             raise FeatureUnavailableError('USB boot unavilable on %s' %
    381                                           platform)
    382         _send_debugd_command(self._host, 'EnableBootFromUsb')
    383 
    384 
    385     def disable(self):
    386         platform = self._host.get_platform().lower()
    387         if any(p in platform for p in self.DISABLE_UNAVAILABLE_PLATFORMS):
    388             raise FeatureUnavailableError("Can't disable USB boot on %s" %
    389                                           platform)
    390         self._host.run('crossystem dev_boot_usb=0')
    391 
    392 
    393     def save_state(self):
    394         self.initial_state = self.is_enabled()
    395 
    396 
    397     def restore_state(self):
    398         if self.initial_state:
    399             self.enable()
    400         else:
    401             self.disable()
    402 
    403 
    404 class SshServerTool(DevTool):
    405     """
    406     SSH server tool.
    407 
    408     SSH configuration has two components, the init file and the test
    409     keys. Since a system could potentially have none, just the init
    410     file, or all files, we want to be sure to restore just the files
    411     that existed before the test started.
    412     """
    413 
    414 
    415     PATHS = ('/etc/init/openssh-server.conf',
    416              '/root/.ssh/authorized_keys',
    417              '/root/.ssh/id_rsa',
    418              '/root/.ssh/id_rsa.pub')
    419 
    420 
    421     def is_enabled(self):
    422         return self._check_enabled(DEV_FEATURE_SSH_SERVER_CONFIGURED)
    423 
    424 
    425     def enable(self):
    426         _send_debugd_command(self._host, 'ConfigureSshServer')
    427 
    428 
    429     def disable(self):
    430         for path in self.PATHS:
    431             self._host.run('rm -f %s' % path)
    432 
    433 
    434     def save_state(self):
    435         self._save_files(self.PATHS)
    436 
    437 
    438     def restore_state(self):
    439         self._restore_files(self.PATHS)
    440 
    441 
    442 class SystemPasswordTool(DevTool):
    443     """
    444     System password configuration tool.
    445 
    446     This tool just affects the system password (/etc/shadow). We could
    447     add a devmode password tool if we want to explicitly test that as
    448     well.
    449     """
    450 
    451 
    452     SYSTEM_PATHS = ('/etc/shadow',)
    453     DEV_PATHS = ('/mnt/stateful_partition/etc/devmode.passwd',)
    454 
    455 
    456     def is_enabled(self):
    457         return self._check_enabled(DEV_FEATURE_SYSTEM_ROOT_PASSWORD_SET)
    458 
    459 
    460     def enable(self):
    461         # Save the devmode.passwd file to avoid affecting it.
    462         self._save_files(self.DEV_PATHS)
    463         try:
    464             _send_debugd_command(self._host, 'SetUserPassword',
    465                                  ('string:root', 'string:test0000'))
    466         finally:
    467             # Restore devmode.passwd
    468             self._restore_files(self.DEV_PATHS)
    469 
    470 
    471     def disable(self):
    472         self._host.run('passwd -d root')
    473 
    474 
    475     def save_state(self):
    476         self._save_files(self.SYSTEM_PATHS)
    477 
    478 
    479     def restore_state(self):
    480         self._restore_files(self.SYSTEM_PATHS)
    481