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