| # Copyright 2014 The Chromium OS Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import logging | |
| import os | |
| import common | |
| from autotest_lib.client.common_lib import error | |
| """ | |
| Functions to query and control debugd dev tools. | |
| This file provides a set of functions to check the general state of the | |
| debugd dev tools, and a set of classes to interface to the individual | |
| tools. | |
| Current tool classes are: | |
| RootfsVerificationTool | |
| BootFromUsbTool | |
| SshServerTool | |
| SystemPasswordTool | |
| These classes have functions to check the state and enable/disable the | |
| tool. Some tools may not be able to disable themselves, in which case | |
| an exception will be thrown (for example, RootfsVerificationTool cannot | |
| be disabled). | |
| General usage will look something like this: | |
| # Make sure tools are accessible on the system. | |
| if debugd_dev_tools.are_dev_tools_available(host): | |
| # Create the tool(s) you want to interact with. | |
| tools = [debugd_dev_tools.SshServerTool(), ...] | |
| for tool in tools: | |
| # Initialize tools and save current state. | |
| tool.initialize(host, save_initial_state=True) | |
| # Perform required action with tools. | |
| tool.enable() | |
| # Restore initial tool state. | |
| tool.restore_state() | |
| # Clean up temporary files. | |
| debugd_dev_tools.remove_temp_files() | |
| """ | |
| # Defined in system_api/dbus/service_constants.h. | |
| DEV_FEATURES_DISABLED = 1 << 0 | |
| DEV_FEATURE_ROOTFS_VERIFICATION_REMOVED = 1 << 1 | |
| DEV_FEATURE_BOOT_FROM_USB_ENABLED = 1 << 2 | |
| DEV_FEATURE_SSH_SERVER_CONFIGURED = 1 << 3 | |
| DEV_FEATURE_DEV_MODE_ROOT_PASSWORD_SET = 1 << 4 | |
| DEV_FEATURE_SYSTEM_ROOT_PASSWORD_SET = 1 << 5 | |
| # Location to save temporary files to store and load state. This folder should | |
| # be persistent through a power cycle so we can't use /tmp. | |
| _TEMP_DIR = '/usr/local/autotest/tmp/debugd_dev_tools' | |
| class AccessError(error.CmdError): | |
| """Raised when debugd D-Bus access fails.""" | |
| pass | |
| class FeatureUnavailableError(error.TestNAError): | |
| """Raised when a feature cannot be enabled or disabled.""" | |
| pass | |
| def query_dev_tools_state(host): | |
| """ | |
| Queries debugd for the current dev features state. | |
| @param host: Host device. | |
| @return: Integer debugd query return value. | |
| @raise AccessError: Can't talk to debugd on the host. | |
| """ | |
| result = _send_debugd_command(host, 'QueryDevFeatures') | |
| state = int(result.stdout) | |
| logging.debug('query_dev_tools_state = %d (0x%04X)', state, state) | |
| return state | |
| def are_dev_tools_available(host): | |
| """ | |
| Check if dev tools are available on the host. | |
| @param host: Host device. | |
| @return: True if tools are available, False otherwise. | |
| """ | |
| try: | |
| return query_dev_tools_state(host) != DEV_FEATURES_DISABLED | |
| except AccessError: | |
| return False | |
| def remove_temp_files(host): | |
| """ | |
| Removes all DevTools temporary files and directories. | |
| Any test using dev tools should try to call this just before | |
| exiting to erase any temporary files that may have been saved. | |
| @param host: Host device. | |
| """ | |
| host.run('rm -rf "%s"' % _TEMP_DIR) | |
| def expect_access_failure(host, tools): | |
| """ | |
| Verifies that access is denied to all provided tools. | |
| Will check are_dev_tools_available() first to try to avoid changing | |
| device state in case access is allowed. Otherwise, the function | |
| will try to enable each tool in the list and throw an exception if | |
| any succeeds. | |
| @param host: Host device. | |
| @param tools: List of tools to checks. | |
| @raise TestFail: are_dev_tools_available() returned True or | |
| a tool successfully enabled. | |
| """ | |
| if are_dev_tools_available(host): | |
| raise error.TestFail('Unexpected dev tool access success') | |
| for tool in tools: | |
| try: | |
| tool.enable() | |
| except AccessError: | |
| # We want an exception, otherwise the tool succeeded. | |
| pass | |
| else: | |
| raise error.TestFail('Unexpected %s enable success.' % tool) | |
| def _send_debugd_command(host, name, args=()): | |
| """ | |
| Sends a debugd command. | |
| @param host: Host to run the command on. | |
| @param name: String debugd D-Bus function name. | |
| @param args: List of string arguments to pass to dbus-send. | |
| @return: The dbus-send CmdResult object. | |
| @raise AccessError: debugd call returned an error. | |
| """ | |
| command = ('dbus-send --system --fixed --print-reply ' | |
| '--dest=org.chromium.debugd /org/chromium/debugd ' | |
| '"org.chromium.debugd.%s"' % name) | |
| for arg in args: | |
| command += ' %s' % arg | |
| try: | |
| return host.run(command) | |
| except error.CmdError as e: | |
| raise AccessError(e.command, e.result_obj, e.additional_text) | |
| class DevTool(object): | |
| """ | |
| Parent tool class. | |
| Each dev tool has its own child class that handles the details | |
| of disabling, enabling, and querying the functionality. This class | |
| provides some common functionality needed by multiple tools. | |
| Child classes should implement the following: | |
| - is_enabled(): use debugd to query whether the tool is enabled. | |
| - enable(): use debugd to enable the tool. | |
| - disable(): manually disable the tool. | |
| - save_state(): record the current tool state on the host. | |
| - restore_state(): restore the saved tool state. | |
| If a child class cannot perform the required action (for | |
| example the rootfs tool can't currently restore its initial | |
| state), leave the function unimplemented so it will throw an | |
| exception if a test attempts to use it. | |
| """ | |
| def initialize(self, host, save_initial_state=False): | |
| """ | |
| Sets up the initial tool state. This must be called on | |
| every tool before use. | |
| @param host: Device host the test is running on. | |
| @param save_initial_state: True to save the device state. | |
| """ | |
| self._host = host | |
| if save_initial_state: | |
| self.save_state() | |
| def is_enabled(self): | |
| """ | |
| Each tool should override this to query itself using debugd. | |
| Normally this can be done by using the provided | |
| _check_enabled() function. | |
| """ | |
| self._unimplemented_function_error('is_enabled') | |
| def enable(self): | |
| """ | |
| Each tool should override this to enable itself using debugd. | |
| """ | |
| self._unimplemented_function_error('enable') | |
| def disable(self): | |
| """ | |
| Each tool should override this to disable itself. | |
| """ | |
| self._unimplemented_function_error('disable') | |
| def save_state(self): | |
| """ | |
| Save the initial tool state. Should be overridden by child | |
| tool classes. | |
| """ | |
| self._unimplemented_function_error('_save_state') | |
| def restore_state(self): | |
| """ | |
| Restore the initial tool state. Should be overridden by child | |
| tool classes. | |
| """ | |
| self._unimplemented_function_error('_restore_state') | |
| def _check_enabled(self, bits): | |
| """ | |
| Checks if the given feature is currently enabled according to | |
| the debugd status query function. | |
| @param bits: Integer status bits corresponding to the features. | |
| @return: True if the status query is enabled and the | |
| indicated bits are all set, False otherwise. | |
| """ | |
| state = query_dev_tools_state(self._host) | |
| enabled = bool((state != DEV_FEATURES_DISABLED) and | |
| (state & bits == bits)) | |
| logging.debug('%s _check_enabled = %s (0x%04X / 0x%04X)', | |
| self, enabled, state, bits) | |
| return enabled | |
| def _get_temp_path(self, source_path): | |
| """ | |
| Get temporary storage path for a file or directory. | |
| Temporary path is based on the tool class name and the | |
| source directory to keep tool files isolated and prevent | |
| name conflicts within tools. | |
| The function returns a full temporary path corresponding to | |
| |source_path|. | |
| For example, _get_temp_path('/foo/bar.txt') would return | |
| '/path/to/temp/folder/debugd_dev_tools/FooTool/foo/bar.txt'. | |
| @param source_path: String path to the file or directory. | |
| @return: Temp path string. | |
| """ | |
| return '%s/%s/%s' % (_TEMP_DIR, self, source_path) | |
| def _save_files(self, paths): | |
| """ | |
| Saves a set of files to a temporary location. | |
| This can be used to save specific files so that a tool can | |
| save its current state before starting a test. | |
| See _restore_files() for restoring the saved files. | |
| @param paths: List of string paths to save. | |
| """ | |
| for path in paths: | |
| temp_path = self._get_temp_path(path) | |
| self._host.run('mkdir -p "%s"' % os.path.dirname(temp_path)) | |
| self._host.run('cp -r "%s" "%s"' % (path, temp_path), | |
| ignore_status=True) | |
| def _restore_files(self, paths): | |
| """ | |
| Restores saved files to their original location. | |
| Used to restore files that have previously been saved by | |
| _save_files(), usually to return the device to its initial | |
| state. | |
| This function does not erase the saved files, so it can | |
| be used multiple times if needed. | |
| @param paths: List of string paths to restore. | |
| """ | |
| for path in paths: | |
| self._host.run('rm -rf "%s"' % path) | |
| self._host.run('cp -r "%s" "%s"' % (self._get_temp_path(path), | |
| path), | |
| ignore_status=True) | |
| def _unimplemented_function_error(self, function_name): | |
| """ | |
| Throws an exception if a required tool function hasn't been | |
| implemented. | |
| """ | |
| raise FeatureUnavailableError('%s has not implemented %s()' % | |
| (self, function_name)) | |
| def __str__(self): | |
| """ | |
| Tool name accessor for temporary files and logging. | |
| Based on class rather than unique instance naming since all | |
| instances of the same tool have identical functionality. | |
| """ | |
| return type(self).__name__ | |
| class RootfsVerificationTool(DevTool): | |
| """ | |
| Rootfs verification removal tool. | |
| This tool is currently unable to transition from non-verified back | |
| to verified rootfs; it may potentially require re-flashing an OS. | |
| Since devices in the test lab run in verified mode, this tool is | |
| unsuitable for automated testing until this capability is | |
| implemented. | |
| """ | |
| def is_enabled(self): | |
| return self._check_enabled(DEV_FEATURE_ROOTFS_VERIFICATION_REMOVED) | |
| def enable(self): | |
| _send_debugd_command(self._host, 'RemoveRootfsVerification') | |
| self._host.reboot() | |
| def disable(self): | |
| raise FeatureUnavailableError('Cannot re-enable rootfs verification') | |
| class BootFromUsbTool(DevTool): | |
| """ | |
| USB boot configuration tool. | |
| Certain boards have restrictions with USB booting. Mario can't | |
| boot from USB at all, and Alex/ZGB can't disable USB booting | |
| once it's been enabled. Any attempts to perform these operation | |
| will raise a FeatureUnavailableError exception. | |
| """ | |
| # Lists of which platforms can't enable or disable USB booting. | |
| ENABLE_UNAVAILABLE_PLATFORMS = ('mario',) | |
| DISABLE_UNAVAILABLE_PLATFORMS = ('mario', 'alex', 'zgb') | |
| def is_enabled(self): | |
| return self._check_enabled(DEV_FEATURE_BOOT_FROM_USB_ENABLED) | |
| def enable(self): | |
| platform = self._host.get_platform().lower() | |
| if any(p in platform for p in self.ENABLE_UNAVAILABLE_PLATFORMS): | |
| raise FeatureUnavailableError('USB boot unavilable on %s' % | |
| platform) | |
| _send_debugd_command(self._host, 'EnableBootFromUsb') | |
| def disable(self): | |
| platform = self._host.get_platform().lower() | |
| if any(p in platform for p in self.DISABLE_UNAVAILABLE_PLATFORMS): | |
| raise FeatureUnavailableError("Can't disable USB boot on %s" % | |
| platform) | |
| self._host.run('crossystem dev_boot_usb=0') | |
| def save_state(self): | |
| self.initial_state = self.is_enabled() | |
| def restore_state(self): | |
| if self.initial_state: | |
| self.enable() | |
| else: | |
| self.disable() | |
| class SshServerTool(DevTool): | |
| """ | |
| SSH server tool. | |
| SSH configuration has two components, the init file and the test | |
| keys. Since a system could potentially have none, just the init | |
| file, or all files, we want to be sure to restore just the files | |
| that existed before the test started. | |
| """ | |
| PATHS = ('/etc/init/openssh-server.conf', | |
| '/root/.ssh/authorized_keys', | |
| '/root/.ssh/id_rsa', | |
| '/root/.ssh/id_rsa.pub') | |
| def is_enabled(self): | |
| return self._check_enabled(DEV_FEATURE_SSH_SERVER_CONFIGURED) | |
| def enable(self): | |
| _send_debugd_command(self._host, 'ConfigureSshServer') | |
| def disable(self): | |
| for path in self.PATHS: | |
| self._host.run('rm -f %s' % path) | |
| def save_state(self): | |
| self._save_files(self.PATHS) | |
| def restore_state(self): | |
| self._restore_files(self.PATHS) | |
| class SystemPasswordTool(DevTool): | |
| """ | |
| System password configuration tool. | |
| This tool just affects the system password (/etc/shadow). We could | |
| add a devmode password tool if we want to explicitly test that as | |
| well. | |
| """ | |
| SYSTEM_PATHS = ('/etc/shadow',) | |
| DEV_PATHS = ('/mnt/stateful_partition/etc/devmode.passwd',) | |
| def is_enabled(self): | |
| return self._check_enabled(DEV_FEATURE_SYSTEM_ROOT_PASSWORD_SET) | |
| def enable(self): | |
| # Save the devmode.passwd file to avoid affecting it. | |
| self._save_files(self.DEV_PATHS) | |
| try: | |
| _send_debugd_command(self._host, 'SetUserPassword', | |
| ('string:root', 'string:test0000')) | |
| finally: | |
| # Restore devmode.passwd | |
| self._restore_files(self.DEV_PATHS) | |
| def disable(self): | |
| self._host.run('passwd -d root') | |
| def save_state(self): | |
| self._save_files(self.SYSTEM_PATHS) | |
| def restore_state(self): | |
| self._restore_files(self.SYSTEM_PATHS) |