1# Copyright 2016 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
8import common
9from autotest_lib.client.common_lib import hosts
10from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
11from autotest_lib.server.hosts import repair
12
13
14class _UpdateVerifier(hosts.Verifier):
15    """
16    Verifier to trigger a servo host update, if necessary.
17
18    The operation doesn't wait for the update to complete and is
19    considered a success whether or not the servo is currently
20    up-to-date.
21    """
22
23    def verify(self, host):
24        # First, only run this verifier if the host is in the physical lab.
25        # Secondly, skip if the test is being run by test_that, because subnet
26        # restrictions can cause the update to fail.
27        if host.is_in_lab() and host.job and host.job.in_lab:
28            host.update_image(wait_for_update=False)
29
30    @property
31    def description(self):
32        return 'servo host software is up-to-date'
33
34
35class _ConfigVerifier(hosts.Verifier):
36    """
37    Base verifier for the servo config file verifiers.
38    """
39
40    CONFIG_FILE = '/var/lib/servod/config'
41    ATTR = ''
42
43    @staticmethod
44    def _get_config_val(host, config_file, attr):
45        """
46        Get the `attr` for `host` from `config_file`.
47
48        @param host         Host to be checked for `config_file`.
49        @param config_file  Path to the config file to be tested.
50        @param attr         Attribute to get from config file.
51
52        @return The attr val as set in the config file, or `None` if
53                the file was absent.
54        """
55        getboard = ('CONFIG=%s ; [ -f $CONFIG ] && '
56                    '. $CONFIG && echo $%s' % (config_file, attr))
57        attr_val = host.run(getboard, ignore_status=True).stdout
58        return attr_val.strip('\n') if attr_val else None
59
60    @staticmethod
61    def _validate_attr(host, val, expected_val, attr, config_file):
62        """
63        Check that the attr setting is valid for the host.
64
65        This presupposes that a valid config file was found.  Raise an
66        execption if:
67          * There was no attr setting from the file (i.e. the setting
68            is an empty string), or
69          * The attr setting is valid, the attr is known,
70            and the setting doesn't match the DUT.
71
72        @param host         Host to be checked for `config_file`.
73        @param val          Value to be tested.
74        @param expected_val Expected value.
75        @param attr         Attribute we're validating.
76        @param config_file  Path to the config file to be tested.
77        """
78        if not val:
79            raise hosts.AutoservVerifyError(
80                    'config file %s exists, but %s '
81                    'is not set' % (attr, config_file))
82        if expected_val is not None and val != expected_val:
83            raise hosts.AutoservVerifyError(
84                    '%s is %s; it should be %s' % (attr, val, expected_val))
85
86
87    def _get_configs(self, host):
88        """
89        Return all the config files to check.
90
91        @param host     Host object.
92
93        @return The list of config files to check.
94        """
95        # TODO(jrbarnette):  Testing `CONFIG_FILE` without a port number
96        # is a legacy.  Ideally, we would force all servos in the lab to
97        # update, and then remove this case.
98        config_list = ['%s_%d' % (self.CONFIG_FILE, host.servo_port)]
99        if host.servo_port == host.DEFAULT_PORT:
100            config_list.append(self.CONFIG_FILE)
101        return config_list
102
103    @property
104    def description(self):
105        return 'servo %s setting is correct' % self.ATTR
106
107
108class _SerialConfigVerifier(_ConfigVerifier):
109    """
110    Verifier for the servo SERIAL configuration.
111    """
112
113    ATTR = 'SERIAL'
114
115    def verify(self, host):
116        """
117        Test whether the `host` has a `SERIAL` setting configured.
118
119        This tests the config file names used by the `servod` upstart
120        job for a valid setting of the `SERIAL` variable.  The following
121        conditions raise errors:
122          * The SERIAL setting doesn't match the DUT's entry in the AFE
123            database.
124          * There is no config file.
125        """
126        if not host.is_cros_host():
127            return
128        # Not all servo hosts will have a servo serial so don't verify if it's
129        # not set.
130        if host.servo_serial is None:
131            return
132        for config in self._get_configs(host):
133            serialval = self._get_config_val(host, config, self.ATTR)
134            if serialval is not None:
135                self._validate_attr(host, serialval, host.servo_serial,
136                                    self.ATTR, config)
137                return
138        msg = 'Servo serial is unconfigured; should be %s' % host.servo_serial
139        raise hosts.AutoservVerifyError(msg)
140
141
142
143class _BoardConfigVerifier(_ConfigVerifier):
144    """
145    Verifier for the servo BOARD configuration.
146    """
147
148    ATTR = 'BOARD'
149
150    def verify(self, host):
151        """
152        Test whether the `host` has a `BOARD` setting configured.
153
154        This tests the config file names used by the `servod` upstart
155        job for a valid setting of the `BOARD` variable.  The following
156        conditions raise errors:
157          * A config file exists, but the content contains no setting
158            for BOARD.
159          * The BOARD setting doesn't match the DUT's entry in the AFE
160            database.
161          * There is no config file.
162        """
163        if not host.is_cros_host():
164            return
165        for config in self._get_configs(host):
166            boardval = self._get_config_val(host, config, self.ATTR)
167            if boardval is not None:
168                self._validate_attr(host, boardval, host.servo_board, self.ATTR,
169                                    config)
170                return
171        msg = 'Servo board is unconfigured'
172        if host.servo_board is not None:
173            msg += '; should be %s' % host.servo_board
174        raise hosts.AutoservVerifyError(msg)
175
176
177class _ServodJobVerifier(hosts.Verifier):
178    """
179    Verifier to check that the `servod` upstart job is running.
180    """
181
182    def verify(self, host):
183        if not host.is_cros_host():
184            return
185        status_cmd = 'status servod PORT=%d' % host.servo_port
186        job_status = host.run(status_cmd, ignore_status=True).stdout
187        if 'start/running' not in job_status:
188            raise hosts.AutoservVerifyError(
189                    'servod not running on %s port %d' %
190                    (host.hostname, host.servo_port))
191
192    @property
193    def description(self):
194        return 'servod upstart job is running'
195
196
197class _ServodConnectionVerifier(hosts.Verifier):
198    """
199    Verifier to check that we can connect to `servod`.
200
201    This tests the connection to the target servod service with a simple
202    method call.  As a side-effect, all servo signals are initialized to
203    default values.
204
205    N.B. Initializing servo signals is necessary because the power
206    button and lid switch verifiers both test against expected initial
207    values.
208    """
209
210    def verify(self, host):
211        host.connect_servo()
212
213    @property
214    def description(self):
215        return 'servod service is taking calls'
216
217
218class _PowerButtonVerifier(hosts.Verifier):
219    """
220    Verifier to check sanity of the `pwr_button` signal.
221
222    Tests that the `pwr_button` signal shows the power button has been
223    released.  When `pwr_button` is stuck at `press`, it commonly
224    indicates that the ribbon cable is disconnected.
225    """
226    # TODO (crbug.com/646593) - Remove list below once servo has been updated
227    # with a dummy pwr_button signal.
228    _BOARDS_WO_PWR_BUTTON = ['arkham', 'storm', 'whirlwind', 'gale']
229
230    def verify(self, host):
231        if host.servo_board in self._BOARDS_WO_PWR_BUTTON:
232            return
233        button = host.get_servo().get('pwr_button')
234        if button != 'release':
235            raise hosts.AutoservVerifyError(
236                    'Check ribbon cable: \'pwr_button\' is stuck')
237
238    @property
239    def description(self):
240        return 'pwr_button control is normal'
241
242
243class _LidVerifier(hosts.Verifier):
244    """
245    Verifier to check sanity of the `lid_open` signal.
246    """
247
248    def verify(self, host):
249        lid_open = host.get_servo().get('lid_open')
250        if lid_open != 'yes' and lid_open != 'not_applicable':
251            raise hosts.AutoservVerifyError(
252                    'Check lid switch: lid_open is %s' % lid_open)
253
254    @property
255    def description(self):
256        return 'lid_open control is normal'
257
258
259class _RestartServod(hosts.RepairAction):
260    """Restart `servod` with the proper BOARD setting."""
261
262    def repair(self, host):
263        if not host.is_cros_host():
264            raise hosts.AutoservRepairError(
265                    'Can\'t restart servod: not running '
266                    'embedded Chrome OS.')
267        host.run('stop servod PORT=%d || true' % host.servo_port)
268        serial = 'SERIAL=%s' % host.servo_serial if host.servo_serial else ''
269        if host.servo_board:
270            host.run('start servod BOARD=%s PORT=%d %s' %
271                     (host.servo_board, host.servo_port, serial))
272        else:
273            # TODO(jrbarnette):  It remains to be seen whether
274            # this action is the right thing to do...
275            logging.warning('Board for DUT is unknown; starting '
276                            'servod assuming a pre-configured '
277                            'board.')
278            host.run('start servod PORT=%d %s' % (host.servo_port, serial))
279        # There's a lag between when `start servod` completes and when
280        # the _ServodConnectionVerifier trigger can actually succeed.
281        # The call to time.sleep() below gives time to make sure that
282        # the trigger won't fail after we return.
283        #
284        # The delay selection was based on empirical testing against
285        # servo V3 on a desktop:
286        #   + 10 seconds was usually too slow; 11 seconds was
287        #     usually fast enough.
288        #   + So, the 20 second delay is about double what we
289        #     expect to need.
290        time.sleep(20)
291
292
293    @property
294    def description(self):
295        return 'Start servod with the proper config settings.'
296
297
298class _ServoRebootRepair(repair.RebootRepair):
299    """
300    Reboot repair action that also waits for an update.
301
302    This is the same as the standard `RebootRepair`, but for
303    a servo host, if there's a pending update, we wait for that
304    to complete before rebooting.  This should ensure that the
305    servo is up-to-date after reboot.
306    """
307
308    def repair(self, host):
309        if host.is_localhost() or not host.is_cros_host():
310            raise hosts.AutoservRepairError(
311                'Target servo is not a test lab servo')
312        host.update_image(wait_for_update=True)
313        afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
314        dut_list = host.get_attached_duts(afe)
315        if len(dut_list) > 1:
316            host.schedule_synchronized_reboot(dut_list, afe, force_reboot=True)
317        else:
318            super(_ServoRebootRepair, self).repair(host)
319
320    @property
321    def description(self):
322        return 'Wait for update, then reboot servo host.'
323
324
325def create_servo_repair_strategy():
326    """
327    Return a `RepairStrategy` for a `ServoHost`.
328    """
329    config = ['brd_config', 'ser_config']
330    verify_dag = [
331        (repair.SshVerifier,         'servo_ssh',   []),
332        (_UpdateVerifier,            'update',      ['servo_ssh']),
333        (_BoardConfigVerifier,       'brd_config',  ['servo_ssh']),
334        (_SerialConfigVerifier,      'ser_config',  ['servo_ssh']),
335        (_ServodJobVerifier,         'job',         config),
336        (_ServodConnectionVerifier,  'servod',      ['job']),
337        (_PowerButtonVerifier,       'pwr_button',  ['servod']),
338        (_LidVerifier,               'lid_open',    ['servod']),
339        # TODO(jrbarnette):  We want a verifier for whether there's
340        # a working USB stick plugged into the servo.  However,
341        # although we always want to log USB stick problems, we don't
342        # want to fail the servo because we don't want a missing USB
343        # stick to prevent, say, power cycling the DUT.
344        #
345        # So, it may be that the right fix is to put diagnosis into
346        # ServoInstallRepair rather than add a verifier.
347    ]
348
349    servod_deps = ['job', 'servod', 'pwr_button', 'lid_open']
350    repair_actions = [
351        (repair.RPMCycleRepair, 'rpm', [], ['servo_ssh']),
352        (_RestartServod, 'restart', ['servo_ssh'], config + servod_deps),
353        (_ServoRebootRepair, 'reboot', ['servo_ssh'], servod_deps),
354    ]
355    return hosts.RepairStrategy(verify_dag, repair_actions)
356