mode_switcher.py revision 3dbc70ac3c58465b4c35b5d09ab10d0e28abde7f
1# Copyright 2015 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 5import logging 6import time 7 8 9class ConnectionError(Exception): 10 """Raised on an error of connecting DUT.""" 11 pass 12 13 14class _BaseFwBypasser(object): 15 """Base class that controls bypass logic for firmware screens.""" 16 17 def __init__(self, servo, faft_config): 18 self.servo = servo 19 self.faft_config = faft_config 20 21 22 def bypass_dev_mode(self): 23 """Bypass the dev mode firmware logic to boot internal image.""" 24 raise NotImplementedError 25 26 27 def bypass_dev_boot_usb(self): 28 """Bypass the dev mode firmware logic to boot USB.""" 29 raise NotImplementedError 30 31 32 def bypass_rec_mode(self): 33 """Bypass the rec mode firmware logic to boot USB.""" 34 raise NotImplementedError 35 36 37 def trigger_dev_to_rec(self): 38 """Trigger to the rec mode from the dev screen.""" 39 raise NotImplementedError 40 41 42 def trigger_rec_to_dev(self): 43 """Trigger to the dev mode from the rec screen.""" 44 raise NotImplementedError 45 46 47 def trigger_dev_to_normal(self): 48 """Trigger to the normal mode from the dev screen.""" 49 raise NotImplementedError 50 51 52class _CtrlDBypasser(_BaseFwBypasser): 53 """Controls bypass logic via Ctrl-D combo.""" 54 55 def bypass_dev_mode(self): 56 """Bypass the dev mode firmware logic to boot internal image.""" 57 time.sleep(self.faft_config.firmware_screen) 58 self.servo.ctrl_d() 59 60 61 def bypass_dev_boot_usb(self): 62 """Bypass the dev mode firmware logic to boot USB.""" 63 time.sleep(self.faft_config.firmware_screen) 64 self.servo.ctrl_u() 65 66 67 def bypass_rec_mode(self): 68 """Bypass the rec mode firmware logic to boot USB.""" 69 self.servo.switch_usbkey('host') 70 time.sleep(self.faft_config.usb_plug) 71 self.servo.switch_usbkey('dut') 72 73 74 def trigger_dev_to_rec(self): 75 """Trigger to the rec mode from the dev screen.""" 76 time.sleep(self.faft_config.firmware_screen) 77 78 # Pressing Enter for too long triggers a second key press. 79 # Let's press it without delay 80 self.servo.enter_key(press_secs=0) 81 82 # For Alex/ZGB, there is a dev warning screen in text mode. 83 # Skip it by pressing Ctrl-D. 84 if self.faft_config.need_dev_transition: 85 time.sleep(self.faft_config.legacy_text_screen) 86 self.servo.ctrl_d() 87 88 89 def trigger_rec_to_dev(self): 90 """Trigger to the dev mode from the rec screen.""" 91 time.sleep(self.faft_config.firmware_screen) 92 self.servo.ctrl_d() 93 time.sleep(self.faft_config.confirm_screen) 94 if self.faft_config.rec_button_dev_switch: 95 logging.info('RECOVERY button pressed to switch to dev mode') 96 self.servo.toggle_recovery_switch() 97 else: 98 logging.info('ENTER pressed to switch to dev mode') 99 self.servo.enter_key() 100 101 102 def trigger_dev_to_normal(self): 103 """Trigger to the normal mode from the dev screen.""" 104 time.sleep(self.faft_config.firmware_screen) 105 self.servo.enter_key() 106 time.sleep(self.faft_config.confirm_screen) 107 self.servo.enter_key() 108 109 110class _JetstreamBypasser(_BaseFwBypasser): 111 """Controls bypass logic of Jetstream devices.""" 112 113 def bypass_dev_mode(self): 114 """Bypass the dev mode firmware logic to boot internal image.""" 115 # Jetstream does nothing to bypass. 116 pass 117 118 119 def bypass_dev_boot_usb(self): 120 """Bypass the dev mode firmware logic to boot USB.""" 121 # TODO: Confirm if it is a proper way to trigger dev boot USB. 122 # We can't verify it this time due to a bug that always boots into 123 # USB on dev mode. 124 self.servo.enable_development_mode() 125 self.servo.switch_usbkey('dut') 126 time.sleep(self.faft_config.firmware_screen) 127 self.servo.toggle_development_switch() 128 129 130 def bypass_rec_mode(self): 131 """Bypass the rec mode firmware logic to boot USB.""" 132 self.servo.switch_usbkey('host') 133 time.sleep(self.faft_config.usb_plug) 134 self.servo.switch_usbkey('dut') 135 136 137 def trigger_dev_to_rec(self): 138 """Trigger to the rec mode from the dev screen.""" 139 # Jetstream does not have this triggering logic. 140 raise NotImplementedError 141 142 143 def trigger_rec_to_dev(self): 144 """Trigger to the dev mode from the rec screen.""" 145 self.servo.disable_development_mode() 146 time.sleep(self.faft_config.firmware_screen) 147 self.servo.toggle_development_switch() 148 149 150 def trigger_dev_to_normal(self): 151 """Trigger to the normal mode from the dev screen.""" 152 # Jetstream does not have this triggering logic. 153 raise NotImplementedError 154 155 156def _create_fw_bypasser(servo, faft_config): 157 """Creates a proper firmware bypasser. 158 159 @param servo: A servo object controlling the servo device. 160 @param faft_config: A FAFT config object, which describes the type of 161 firmware bypasser. 162 """ 163 bypasser_type = faft_config.fw_bypasser_type 164 if bypasser_type == 'ctrl_d_bypasser': 165 logging.info('Create a CtrlDBypasser') 166 return _CtrlDBypasser(servo, faft_config) 167 elif bypasser_type == 'jetstream_bypasser': 168 logging.info('Create a JetstreamBypasser') 169 return _JetstreamBypasser(servo, faft_config) 170 elif bypasser_type == 'ryu_bypasser': 171 # FIXME Create an RyuBypasser 172 logging.info('Create a CtrlDBypasser') 173 return _CtrlDBypasser(servo, faft_config) 174 else: 175 raise NotImplementedError('Not supported fw_bypasser_type: %s', 176 bypasser_type) 177 178 179class _BaseModeSwitcher(object): 180 """Base class that controls firmware mode switching.""" 181 182 def __init__(self, faft_framework): 183 self.faft_framework = faft_framework 184 self.client_host = faft_framework._client 185 self.faft_client = faft_framework.faft_client 186 self.servo = faft_framework.servo 187 self.faft_config = faft_framework.faft_config 188 self.checkers = faft_framework.checkers 189 self.bypasser = _create_fw_bypasser(self.servo, self.faft_config) 190 self._backup_mode = None 191 192 193 def setup_mode(self, mode): 194 """Setup for the requested mode. 195 196 It makes sure the system in the requested mode. If not, it tries to 197 do so. 198 199 @param mode: A string of mode, one of 'normal', 'dev', or 'rec'. 200 """ 201 if not self.checkers.mode_checker(mode): 202 logging.info('System not in expected %s mode. Reboot into it.', 203 mode) 204 if self._backup_mode is None: 205 # Only resume to normal/dev mode after test, not recovery. 206 self._backup_mode = 'dev' if mode == 'normal' else 'normal' 207 self.reboot_to_mode(mode) 208 209 210 def restore_mode(self): 211 """Restores original dev mode status if it has changed.""" 212 if self._backup_mode is not None: 213 self.reboot_to_mode(self._backup_mode) 214 215 216 def reboot_to_mode(self, to_mode, from_mode=None, sync_before_boot=True, 217 wait_for_dut_up=True): 218 """Reboot and execute the mode switching sequence. 219 220 @param to_mode: The target mode, one of 'normal', 'dev', or 'rec'. 221 @param from_mode: The original mode, optional, one of 'normal, 'dev', 222 or 'rec'. 223 @param sync_before_boot: True to sync to disk before booting. 224 @param wait_for_dut_up: True to wait DUT online again. False to do the 225 reboot and mode switching sequence only and may 226 need more operations to pass the firmware 227 screen. 228 """ 229 logging.info('-[ModeSwitcher]-[ start reboot_to_mode(%r, %r, %r) ]-', 230 to_mode, from_mode, wait_for_dut_up) 231 if sync_before_boot: 232 self.faft_framework.blocking_sync() 233 if to_mode == 'rec': 234 self._enable_rec_mode_and_reboot(usb_state='dut') 235 if wait_for_dut_up: 236 self.bypasser.bypass_rec_mode() 237 self.wait_for_client() 238 239 elif to_mode == 'dev': 240 self._enable_dev_mode_and_reboot() 241 if wait_for_dut_up: 242 self.bypasser.bypass_dev_mode() 243 self.wait_for_client() 244 245 elif to_mode == 'normal': 246 self._enable_normal_mode_and_reboot() 247 if wait_for_dut_up: 248 self.wait_for_client() 249 250 else: 251 raise NotImplementedError( 252 'Not supported mode switching from %s to %s' % 253 (str(from_mode), to_mode)) 254 logging.info('-[ModeSwitcher]-[ end reboot_to_mode(%r, %r, %r) ]-', 255 to_mode, from_mode, wait_for_dut_up) 256 257 258 def mode_aware_reboot(self, reboot_type=None, reboot_method=None, 259 sync_before_boot=True, wait_for_dut_up=True): 260 """Uses a mode-aware way to reboot DUT. 261 262 For example, if DUT is in dev mode, it requires pressing Ctrl-D to 263 bypass the developer screen. 264 265 @param reboot_type: A string of reboot type, one of 'warm', 'cold', or 266 'custom'. Default is a warm reboot. 267 @param reboot_method: A custom method to do the reboot. Only use it if 268 reboot_type='custom'. 269 @param sync_before_boot: True to sync to disk before booting. 270 @param wait_for_dut_up: True to wait DUT online again. False to do the 271 reboot only. 272 """ 273 if reboot_type is None or reboot_type == 'warm': 274 reboot_method = self.servo.get_power_state_controller().warm_reset 275 elif reboot_type == 'cold': 276 reboot_method = self.servo.get_power_state_controller().reset 277 elif reboot_type != 'custom': 278 raise NotImplementedError('Not supported reboot_type: %s', 279 reboot_type) 280 281 logging.info("-[ModeSwitcher]-[ start mode_aware_reboot(%r, %s, ..) ]-", 282 reboot_type, reboot_method.__name__) 283 is_normal = is_dev = False 284 if sync_before_boot: 285 if wait_for_dut_up: 286 is_normal = self.checkers.mode_checker('normal') 287 is_dev = self.checkers.mode_checker('dev') 288 boot_id = self.faft_framework.get_bootid() 289 self.faft_framework.blocking_sync() 290 reboot_method() 291 if sync_before_boot: 292 self.wait_for_client_offline(orig_boot_id=boot_id) 293 if wait_for_dut_up: 294 # For encapsulating the behavior of skipping firmware screen, 295 # e.g. requiring unplug and plug USB, the variants are not 296 # hard coded in tests. We keep this logic in this 297 # mode_aware_reboot method. 298 if not is_dev: 299 # In the normal/recovery boot flow, replugging USB does not 300 # affect the boot flow. But when something goes wrong, like 301 # firmware corrupted, it automatically leads to a recovery USB 302 # boot. 303 self.servo.switch_usbkey('host') 304 if not is_normal: 305 self.bypasser.bypass_dev_mode() 306 if not is_dev: 307 self.bypasser.bypass_rec_mode() 308 self.wait_for_client() 309 logging.info("-[ModeSwitcher]-[ end mode_aware_reboot(%r, %s, ..) ]-", 310 reboot_type, reboot_method.__name__) 311 312 313 def _enable_rec_mode_and_reboot(self, usb_state=None): 314 """Switch to rec mode and reboot. 315 316 This method emulates the behavior of the old physical recovery switch, 317 i.e. switch ON + reboot + switch OFF, and the new keyboard controlled 318 recovery mode, i.e. just press Power + Esc + Refresh. 319 320 @param usb_state: A string, one of 'dut', 'host', or 'off'. 321 """ 322 psc = self.servo.get_power_state_controller() 323 psc.power_off() 324 if usb_state: 325 self.servo.switch_usbkey(usb_state) 326 psc.power_on(psc.REC_ON) 327 328 329 def _disable_rec_mode_and_reboot(self, usb_state=None): 330 """Disable the rec mode and reboot. 331 332 It is achieved by calling power state controller to do a normal 333 power on. 334 """ 335 psc = self.servo.get_power_state_controller() 336 psc.power_off() 337 psc.power_on(psc.REC_OFF) 338 339 340 def _enable_dev_mode_and_reboot(self): 341 """Switch to developer mode and reboot.""" 342 raise NotImplementedError 343 344 345 def _enable_normal_mode_and_reboot(self): 346 """Switch to normal mode and reboot.""" 347 raise NotImplementedError 348 349 350 # Redirects the following methods to FwBypasser 351 def bypass_dev_mode(self): 352 """Bypass the dev mode firmware logic to boot internal image.""" 353 self.bypasser.bypass_dev_mode() 354 355 356 def bypass_dev_boot_usb(self): 357 """Bypass the dev mode firmware logic to boot USB.""" 358 self.bypasser.bypass_dev_boot_usb() 359 360 361 def bypass_rec_mode(self): 362 """Bypass the rec mode firmware logic to boot USB.""" 363 self.bypasser.bypass_rec_mode() 364 365 366 def trigger_dev_to_rec(self): 367 """Trigger to the rec mode from the dev screen.""" 368 self.bypasser.trigger_dev_to_rec() 369 370 371 def trigger_rec_to_dev(self): 372 """Trigger to the dev mode from the rec screen.""" 373 self.bypasser.trigger_rec_to_dev() 374 375 376 def trigger_dev_to_normal(self): 377 """Trigger to the normal mode from the dev screen.""" 378 self.bypasser.trigger_dev_to_normal() 379 380 381 def wait_for_client(self, timeout=180): 382 """Wait for the client to come back online. 383 384 New remote processes will be launched if their used flags are enabled. 385 386 @param timeout: Time in seconds to wait for the client SSH daemon to 387 come up. 388 @raise ConnectionError: Failed to connect DUT. 389 """ 390 logging.info("-[FAFT]-[ start wait_for_client ]---") 391 # Wait for the system to respond to ping before attempting ssh 392 if not self.client_host.ping_wait_up(timeout): 393 logging.warning("-[FAFT]-[ system did not respond to ping ]") 394 if self.client_host.wait_up(timeout): 395 # Check the FAFT client is avaiable. 396 self.faft_client.system.is_available() 397 # Stop update-engine as it may change firmware/kernel. 398 self.faft_framework._stop_service('update-engine') 399 else: 400 logging.error('wait_for_client() timed out.') 401 raise ConnectionError() 402 logging.info("-[FAFT]-[ end wait_for_client ]-----") 403 404 405 def wait_for_client_offline(self, timeout=60, orig_boot_id=None): 406 """Wait for the client to come offline. 407 408 @param timeout: Time in seconds to wait the client to come offline. 409 @param orig_boot_id: A string containing the original boot id. 410 @raise ConnectionError: Failed to wait DUT offline. 411 """ 412 # When running against panther, we see that sometimes 413 # ping_wait_down() does not work correctly. There needs to 414 # be some investigation to the root cause. 415 # If we sleep for 120s before running get_boot_id(), it 416 # does succeed. But if we change this to ping_wait_down() 417 # there are implications on the wait time when running 418 # commands at the fw screens. 419 if not self.client_host.ping_wait_down(timeout): 420 if orig_boot_id and self.client_host.get_boot_id() != orig_boot_id: 421 logging.warn('Reboot done very quickly.') 422 return 423 raise ConnectionError() 424 425 426class _PhysicalButtonSwitcher(_BaseModeSwitcher): 427 """Class that switches firmware mode via physical button.""" 428 429 def _enable_dev_mode_and_reboot(self): 430 """Switch to developer mode and reboot.""" 431 self.servo.enable_development_mode() 432 self.faft_client.system.run_shell_command( 433 'chromeos-firmwareupdate --mode todev && reboot') 434 435 436 def _enable_normal_mode_and_reboot(self): 437 """Switch to normal mode and reboot.""" 438 self.servo.disable_development_mode() 439 self.faft_client.system.run_shell_command( 440 'chromeos-firmwareupdate --mode tonormal && reboot') 441 442 443class _KeyboardDevSwitcher(_BaseModeSwitcher): 444 """Class that switches firmware mode via keyboard combo.""" 445 446 def _enable_dev_mode_and_reboot(self): 447 """Switch to developer mode and reboot.""" 448 logging.info("Enabling keyboard controlled developer mode") 449 # Rebooting EC with rec mode on. Should power on AP. 450 # Plug out USB disk for preventing recovery boot without warning 451 self._enable_rec_mode_and_reboot(usb_state='host') 452 self.wait_for_client_offline() 453 self.bypasser.trigger_rec_to_dev() 454 455 456 def _enable_normal_mode_and_reboot(self): 457 """Switch to normal mode and reboot.""" 458 logging.info("Disabling keyboard controlled developer mode") 459 self._disable_rec_mode_and_reboot() 460 self.wait_for_client_offline() 461 self.bypasser.trigger_dev_to_normal() 462 463 464class _JetstreamSwitcher(_BaseModeSwitcher): 465 """Class that switches firmware mode in Jetstream devices.""" 466 467 def _enable_dev_mode_and_reboot(self): 468 """Switch to developer mode and reboot.""" 469 logging.info("Enabling Jetstream developer mode") 470 self._enable_rec_mode_and_reboot(usb_state='host') 471 self.wait_for_client_offline() 472 self.bypasser.trigger_rec_to_dev() 473 474 475 def _enable_normal_mode_and_reboot(self): 476 """Switch to normal mode and reboot.""" 477 logging.info("Disabling Jetstream developer mode") 478 self.servo.disable_development_mode() 479 self._enable_rec_mode_and_reboot(usb_state='host') 480 time.sleep(self.faft_config.firmware_screen) 481 self._disable_rec_mode_and_reboot(usb_state='host') 482 483 484class _RyuSwitcher(_BaseModeSwitcher): 485 """Class that switches firmware mode via physical button.""" 486 487 def wait_for_client(self, timeout=180): 488 """Wait for the client to come back online. 489 490 New remote processes will be launched if their used flags are enabled. 491 492 @param timeout: Time in seconds to wait for the client SSH daemon to 493 come up. 494 @raise ConnectionError: Failed to connect DUT. 495 """ 496 if not self.faft_client.system.wait_for_client(timeout): 497 raise ConnectionError() 498 499 500 def wait_for_client_offline(self, timeout=60, orig_boot_id=None): 501 """Wait for the client to come offline. 502 503 @param timeout: Time in seconds to wait the client to come offline. 504 @param orig_boot_id: A string containing the original boot id. 505 @raise ConnectionError: Failed to wait DUT offline. 506 """ 507 # TODO: Add a way to check orig_boot_id 508 if not self.faft_client.system.wait_for_client_offline(timeout): 509 raise ConnectionError() 510 511 512 def _enable_dev_mode_and_reboot(self): 513 """Switch to developer mode and reboot.""" 514 # FIXME Implement switching to dev mode. 515 pass 516 517 518 def _enable_normal_mode_and_reboot(self): 519 """Switch to normal mode and reboot.""" 520 # FIXME Implement switching to normal mode. 521 pass 522 523 524def create_mode_switcher(faft_framework): 525 """Creates a proper mode switcher. 526 527 @param faft_framework: The main FAFT framework object. 528 """ 529 switcher_type = faft_framework.faft_config.mode_switcher_type 530 if switcher_type == 'physical_button_switcher': 531 logging.info('Create a PhysicalButtonSwitcher') 532 return _PhysicalButtonSwitcher(faft_framework) 533 elif switcher_type == 'keyboard_dev_switcher': 534 logging.info('Create a KeyboardDevSwitcher') 535 return _KeyboardDevSwitcher(faft_framework) 536 elif switcher_type == 'jetstream_switcher': 537 logging.info('Create a JetstreamSwitcher') 538 return _JetstreamSwitcher(faft_framework) 539 elif switcher_type == 'ryu_switcher': 540 logging.info('Create a RyuSwitcher') 541 return _RyuSwitcher(faft_framework) 542 else: 543 raise NotImplementedError('Not supported mode_switcher_type: %s', 544 switcher_type) 545