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