• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2018 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"""Autotest for Logitech Meetup firmware updater."""
5
6from __future__ import print_function
7
8import logging
9import os
10import re
11import time
12
13
14from autotest_lib.client.common_lib import error
15from autotest_lib.client.common_lib.cros import power_cycle_usb_util
16from autotest_lib.client.common_lib.cros.cfm.usb import cfm_usb_devices
17from autotest_lib.server import test
18
19
20POWER_CYCLE_WAIT_TIME_SEC = 20
21
22
23class enterprise_CFM_LogitechMeetupUpdater(test.test):
24    """
25    Logitech Meetup firmware test on Chrome For Meeting devices
26    The test follows the following steps
27        1) Check if the filesystem is writable
28           If not make the filesystem writable and reboot
29        2) Backup the existing firmware file on DUT
30        3) Copy the older firmware files to DUT
31        4) Force update older firmware on Meetup Camera
32        5) Restore the original firmware files on DUT
33        4) Power cycle usb port to simulate unplug/replug of device which
34           should initiate a firmware update
35        5) Wait for firmware update to finish and check firmware version
36        6) Cleanup
37
38    """
39
40    version = 1
41
42    def initialize(self, host):
43        """
44        Initializes the class.
45
46        Stores the firmware file path.
47        Gets the board type.
48        Reads the current firmware versions.
49        """
50
51        self.host = host
52        self.log_file = '/tmp/logitech-updater.log'
53        self.fw_path_base = '/lib/firmware/logitech'
54        self.fw_pkg_origin = 'meetup'
55        self.fw_pkg_backup = 'meetup_backup'
56        self.fw_pkg_test = 'meetup_184'
57        self.fw_pkg_files = ['meetup_audio.bin',
58                             'meetup_audio_logicool.bin',
59                             'meetup_ble.bin',
60                             'meetup_codec.bin',
61                             'meetup_eeprom_logicool.s19',
62                             'meetup_eeprom.s19',
63                             'meetup_video.bin',
64                             'meetup_audio.bin.sig',
65                             'meetup_audio_logicool.bin.sig',
66                             'meetup_ble.bin.sig',
67                             'meetup_codec.bin.sig',
68                             'meetup_eeprom_logicool.s19.sig',
69                             'meetup_eeprom.s19.sig',
70                             'meetup_video.bin.sig']
71        self.fw_path_test = os.path.join(self.fw_path_base,
72                                         self.fw_pkg_test)
73        self.fw_path_origin = os.path.join(self.fw_path_base,
74                                           self.fw_pkg_origin)
75        self.fw_path_backup = os.path.join(self.fw_path_base,
76                                           self.fw_pkg_backup)
77        self.board = self.host.get_board().split(':')[1]
78        self.vid = cfm_usb_devices.LOGITECH_MEETUP.vendor_id
79        self.pid = cfm_usb_devices.LOGITECH_MEETUP.product_id
80        self.org_fw_ver = self.get_image_fw_ver()
81
82    def cleanup(self):
83        """
84        Cleanups after tests.
85
86        Removes the test firmware.
87        Restores the original firmware files.
88        Flashes the camera to original firmware if needed.
89        """
90
91        # Delete test firmware package.
92        cmd = 'rm -rf {}'.format(self.fw_path_test)
93        self.host.run(cmd)
94
95        # Delete the symlink created.
96        cmd = 'rm {}'.format(self.fw_path_origin)
97        self.host.run(cmd)
98
99        # Move the backup package back.
100        cmd = 'mv {} {}'.format(self.fw_path_backup, self.fw_path_origin)
101        self.host.run(cmd)
102
103        # Do not leave the camera with test (older) firmware.
104        if not self.is_device_firmware_equal_to(self.org_fw_ver):
105            logging.debug('Meetup device has old firmware after test'
106                          'Flashing new firmware')
107            self.flash_fw()
108
109        super(enterprise_CFM_LogitechMeetupUpdater, self).cleanup()
110
111    def _run_cmd(self, command, ignore_status=True):
112        """
113        Runs command line on DUT, wait for completion and return the output.
114
115        @param command: command line to run in dut.
116        @param ignore_status: if true ignore the status return by command
117
118        @returns the command output
119
120        """
121
122        logging.debug('Execute: %s', command)
123
124        result = self.host.run(command, ignore_status=ignore_status)
125        if result.stderr:
126            output = result.stderr
127        else:
128            output = result.stdout
129        logging.debug('Output: %s', output)
130        return output
131
132    def make_rootfs_writable(self):
133        """Checks and makes root filesystem writable."""
134
135        if not self.is_filesystem_readwrite():
136            logging.info('DUT root file system is not writable. '
137                         'Converting it writable...')
138            self.convert_rootfs_writable()
139        else:
140            logging.info('DUT root file system is writable.')
141
142    def convert_rootfs_writable(self):
143        """Makes DUT rootfs writable."""
144
145        logging.info('Disabling rootfs verification...')
146        self.remove_rootfs_verification()
147
148        logging.info('Rebooting...')
149        self.host.reboot()
150
151        logging.info('Remounting..')
152        cmd = 'mount -o remount,rw /'
153        self.host.run(cmd)
154
155    def remove_rootfs_verification(self):
156        """Removes rootfs verification."""
157
158        # 2 & 4 are default partitions, and the system boots from one of them.
159        # Code from chromite/scripts/deploy_chrome.py
160        KERNEL_A_PARTITION = 2
161        KERNEL_B_PARTITION = 4
162
163        cmd_template = ('/usr/share/vboot/bin/make_dev_ssd.sh'
164                        ' --partitions "%d %d"'
165                        ' --remove_rootfs_verification --force')
166        cmd = cmd_template % (KERNEL_A_PARTITION, KERNEL_B_PARTITION)
167        self.host.run(cmd)
168
169    def is_filesystem_readwrite(self):
170        """Checks if the root file system is writable."""
171
172        # Query the DUT's filesystem /dev/root and check whether it is rw
173
174        cmd = 'cat /proc/mounts | grep "/dev/root"'
175        result = self._run_cmd(cmd)
176        fields = re.split(' |,', result)
177
178        # Result of grep will be of the following format
179        # /dev/root / ext2 ro,seclabel <....truncated...> => readonly
180        # /dev/root / ext2 rw,seclabel <....truncated...> => readwrite
181        is_writable = fields.__len__() >= 4 and fields[3] == 'rw'
182        return is_writable
183
184    def fw_ver_from_output_str(self, cmd_output):
185        """
186        Parse firmware version of logitech-updater output.
187
188        logitech-updater output differs for image_version and device_version
189        This function finds the line which contains string "Meetup" and parses
190        succeding lines. Each line is split on spaces (after collapsing spaces)
191        and index 1 gives component name (ex. Eeprom) and index 3 gives the
192        firmware version (ex. 1.14)
193        The actual output is given below.
194
195        logitech-updater --image_version
196
197        [INFO:main.cc(105)] PTZ Pro 2 Versions:
198        [INFO:main.cc(59)] Video version:  2.0.175
199        [INFO:main.cc(61)] Eeprom version: 1.6
200        [INFO:main.cc(63)] Mcu2 version:   3.9
201
202        [INFO:main.cc(105)] MeetUp Versions:
203        [INFO:main.cc(59)] Video version:  1.0.197
204        [INFO:main.cc(61)] Eeprom version: 1.14
205        [INFO:main.cc(65)] Audio version:  1.0.239
206        [INFO:main.cc(67)] Codec version:  8.0.216
207        [INFO:main.cc(69)] BLE version:    1.0.121
208
209        logitech-updater  --device_version
210
211        [INFO:main.cc(88)] Device name:    Logitech MeetUp
212        [INFO:main.cc(59)] Video version:  1.0.197
213        [INFO:main.cc(61)] Eeprom version: 1.14
214        [INFO:main.cc(65)] Audio version:  1.0.239
215        [INFO:main.cc(67)] Codec version:  8.0.216
216        [INFO:main.cc(69)] BLE version:    1.0.121
217
218
219        """
220
221        logging.debug('Parsing output from updater %s', cmd_output)
222        if 'MeetUp image not found' in cmd_output or 'MeetUp' not in cmd_output:
223            raise error.TestFail('MeetUp image not found on DUT')
224        try:
225            version = {}
226            output = cmd_output.split('\n')
227            start_line = -1
228
229            # Find the line of the output with string "Meetup
230            for i, l in enumerate(output):
231                if 'MeetUp' in l:
232                    start_line = i
233                    break
234
235            if start_line == -1:
236                raise error.TestFail('Meetup version not found'
237                                     ' in updater output')
238
239            output = output[start_line+1:start_line+6]
240            logging.debug('Parsing Meetup firmware info %s', str(output))
241            for l in output:
242
243                # Output lines are of the format
244                # [INFO:main.cc(59)] Video version:  1.0.197
245                l = ' '.join(l.split())  # Collapse multiple spaces to one space
246                parts = l.split(' ')  # parts[1] is "Video" parts[3] is 1.0.197
247                version[parts[1]] = parts[3]
248            logging.debug('Version is %s', str(version))
249            return version
250        except:
251            logging.error('Error while parsing logitech-updater output')
252            raise
253
254    def get_updater_output(self, cmd):
255        """Get updater output while avoiding transient failures."""
256
257        NUM_RETRIES = 3
258        WAIT_TIME = 5
259        for _ in range(NUM_RETRIES):
260            output = self._run_cmd(cmd)
261            if 'Failed to read' in output:
262                time.sleep(WAIT_TIME)
263                continue
264            return output
265
266    def get_image_fw_ver(self):
267        """Get the version of firmware on DUT."""
268
269        output = self.get_updater_output('logitech-updater --image_version'
270                                         ' --log_to=stdout')
271        return self.fw_ver_from_output_str(output)
272
273    def get_device_fw_ver(self):
274        """Get the version of firmware on Meetup device."""
275
276        output = self.get_updater_output('logitech-updater --device_version'
277                                         ' --log_to=stdout')
278        return self.fw_ver_from_output_str(output)
279
280    def copy_test_firmware(self):
281        """Copy test firmware from server to DUT."""
282
283        current_dir = os.path.dirname(os.path.realpath(__file__))
284        src_firmware_path = os.path.join(current_dir, self.fw_pkg_test)
285        dst_firmware_path = self.fw_path_base
286        logging.info('Copy firmware from (%s) to (%s).', src_firmware_path,
287                     dst_firmware_path)
288        self.host.send_file(src_firmware_path, dst_firmware_path,
289                            delete_dest=True)
290
291    def trigger_updater(self):
292        """Trigger udev rule to run fw updater by power cycling the usb."""
293
294        try:
295            power_cycle_usb_util.power_cycle_usb_vidpid(self.host, self.board,
296                                                        self.vid, self.pid)
297        except KeyError:
298            raise error.TestFail('Counld\'t find target device: '
299                                 'vid:pid {}:{}'.format(self.vid, self.pid))
300
301    def wait_for_meetup_device(self):
302        """
303        Wait for Meetup device device to be enumerated.
304
305        Check if a device with given (vid,pid) is present.
306        Timeout after wait_time seconds. Default 30 seconds
307        """
308
309        TIME_SLEEP = 10
310        NUM_ITERATIONS = 3
311        WAIT_TIME = TIME_SLEEP * NUM_ITERATIONS
312
313        logging.debug('Waiting for Meetup device')
314        for _ in range(NUM_ITERATIONS):
315            res = power_cycle_usb_util.get_port_number_from_vidpid(
316                self.host, self.vid, self.pid)
317            (bus_num, port_num) = res
318            if bus_num is not None and port_num is not None:
319                logging.debug('Meetup device detected')
320                return
321            else:
322                logging.debug('Meetup device not detected.'
323                              'Waiting for (%s) seconds', TIME_SLEEP)
324                time.sleep(TIME_SLEEP)
325
326        logging.error('Unable to detect the device after (%s) seconds.'
327                      'Timing out...', WAIT_TIME)
328        raise error.TestFail('Target device not detected.')
329
330    def setup_fw(self, firmware_package):
331        """Setup firmware package that is going to be used for updating."""
332
333        firmware_path = os.path.join(self.fw_path_base, firmware_package)
334        cmd = 'ln -sfn {} {}'.format(firmware_path, self.fw_path_origin)
335        self.host.run(cmd)
336
337    def flash_fw(self, force=False):
338        """Flash certain firmware to device.
339
340        Run logitech firmware updater on DUT to flash the firmware setuped
341        to target device (PTZ Pro 2).
342
343        @param force: run with force update, will bypass fw version check.
344
345        """
346
347        cmd = ('/usr/sbin/logitech-updater --log_to=stdout --update_components'
348               ' --lock')
349        if force:
350            cmd += ' --force'
351        output = self._run_cmd(cmd)
352        return output
353
354    def print_fw_version(self, version, info_str=''):
355        """Pretty print Meetup firmware version."""
356
357        if info_str:
358            print(info_str)
359        print('Video version: ', version['Video'])
360        print('Eeprom version: ', version['Eeprom'])
361        print('Audio version: ', version['Audio'])
362        print('Codec version: ', version['Codec'])
363        print('BLE version: ', version['BLE'])
364
365    def is_device_firmware_equal_to(self, expected_ver):
366        """Check that the device fw version is equal to given version."""
367
368        device_fw_version = self.get_device_fw_ver()
369        if  device_fw_version != expected_ver:
370            logging.error('Device firmware version is not the expected version')
371            self.print_fw_version(device_fw_version, 'Device firmware version')
372            self.print_fw_version(expected_ver, 'Expected firmware version')
373            return False
374        else:
375            return True
376
377    def flash_old_firmware(self):
378        """Flash old (test) version of firmware on the device."""
379
380        # Flash old FW to device.
381        self.setup_fw(self.fw_pkg_test)
382        test_fw_ver = self.get_image_fw_ver()
383        self.print_fw_version(test_fw_ver, 'Test firmware version')
384        output = self.flash_fw(force=True)
385        time.sleep(POWER_CYCLE_WAIT_TIME_SEC)
386        with open(self.log_file, 'w') as f:
387            delim = '-' * 8
388            f.write('{}Log info for writing old firmware{}'
389                    '\n'.format(delim, delim))
390            f.write(output)
391        if not self.is_device_firmware_equal_to(test_fw_ver):
392            raise error.TestFail('Flashing old firmware failed')
393        logging.info('Device flashed with test firmware')
394
395    def backup_original_firmware(self):
396        """Backup existing firmware on DUT."""
397        # Copy old FW to device.
398        cmd = 'mv {} {}'.format(self.fw_path_origin, self.fw_path_backup)
399        self.host.run(cmd)
400
401    def is_updater_running(self):
402        """Checks if the logitech-updater is running."""
403
404        cmd = 'logitech-updater --lock --device_version --log_to=stdout'
405        output = self._run_cmd(cmd)
406        return 'There is another logitech-updater running' in output
407
408    def wait_for_updater(self):
409        """Wait logitech-updater to stop or timeout after 6 minutes."""
410
411        NUM_ITERATION = 12
412        WAIT_TIME = 30  # seconds
413        logging.debug('Wait for any currently running updater to finish')
414        for _ in range(NUM_ITERATION):
415            if self.is_updater_running():
416                logging.debug('logitech-updater is running.'
417                              'Waiting for 30 seconds')
418                time.sleep(WAIT_TIME)
419            else:
420                logging.debug('logitech-updater not running')
421                return
422        logging.error('logitech-updater is still running after 6 minutes')
423
424    def test_firmware_update(self):
425        """Trigger firmware updater and check device firmware version."""
426
427        # Simulate hotplug to run FW updater.
428        logging.info('Setup original firmware')
429        self.setup_fw(self.fw_pkg_backup)
430        logging.info('Simulate hot plugging the device')
431        self.trigger_updater()
432        self.wait_for_meetup_device()
433
434        # The firmware check will fail if the check runs in a short window
435        # between the device being detected and the firmware updater starting.
436        # Adding a delay to reduce the chance of that scenerio.
437        time.sleep(POWER_CYCLE_WAIT_TIME_SEC)
438
439        self.wait_for_updater()
440
441        if not self.is_device_firmware_equal_to(self.org_fw_ver):
442            raise error.TestFail('Camera not updated to new firmware')
443        logging.info('Firmware update was completed successfully')
444
445    def run_once(self):
446        """
447        Entry point for test.
448
449        The following actions are performed in this test.
450        - Device is flashed with older firmware.
451        - Powercycle usb port to simulate hotplug inorder to start the updater.
452        - Check that the device is updated with newer firmware.
453        """
454
455        # Check if updater is already running
456        self.wait_for_updater()
457
458        self.print_fw_version(self.org_fw_ver,
459                              'Original firmware version on DUT')
460        self.print_fw_version(self.get_device_fw_ver(),
461                              'Firmware version on Meetup device')
462
463        self.make_rootfs_writable()
464        self.backup_original_firmware()
465
466        # Flash test firmware version
467        self.copy_test_firmware()
468        self.flash_old_firmware()
469
470        # Test firmware update
471        self.test_firmware_update()
472        logging.info('Logitech Meetup firmware updater test was successful')
473