| # Copyright (c) 2018 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Autotest for Logitech Meetup firmware updater.""" |
| |
| import logging |
| import os |
| import re |
| import time |
| |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib.cros import power_cycle_usb_util |
| from autotest_lib.client.common_lib.cros.cfm.usb import cfm_usb_devices |
| from autotest_lib.server import test |
| |
| |
| POWER_CYCLE_WAIT_TIME_SEC = 20 |
| |
| |
| class enterprise_CFM_LogitechMeetupUpdater(test.test): |
| """ |
| Logitech Meetup firmware test on Chrome For Meeting devices |
| The test follows the following steps |
| 1) Check if the filesystem is writable |
| If not make the filesystem writable and reboot |
| 2) Backup the existing firmware file on DUT |
| 3) Copy the older firmware files to DUT |
| 4) Force update older firmware on Meetup Camera |
| 5) Restore the original firmware files on DUT |
| 4) Power cycle usb port to simulate unplug/replug of device which |
| should initiate a firmware update |
| 5) Wait for firmware update to finish and check firmware version |
| 6) Cleanup |
| |
| """ |
| |
| version = 1 |
| |
| def initialize(self, host): |
| """ |
| Initializes the class. |
| |
| Stores the firmware file path. |
| Gets the board type. |
| Reads the current firmware versions. |
| """ |
| |
| self.host = host |
| self.log_file = '/tmp/logitech-updater.log' |
| self.fw_path_base = '/lib/firmware/logitech' |
| self.fw_pkg_origin = 'meetup' |
| self.fw_pkg_backup = 'meetup_backup' |
| self.fw_pkg_test = 'meetup_184' |
| self.fw_pkg_files = ['meetup_audio.bin', |
| 'meetup_audio_logicool.bin', |
| 'meetup_ble.bin', |
| 'meetup_codec.bin', |
| 'meetup_eeprom_logicool.s19', |
| 'meetup_eeprom.s19', |
| 'meetup_video.bin', |
| 'meetup_audio.bin.sig', |
| 'meetup_audio_logicool.bin.sig', |
| 'meetup_ble.bin.sig', |
| 'meetup_codec.bin.sig', |
| 'meetup_eeprom_logicool.s19.sig', |
| 'meetup_eeprom.s19.sig', |
| 'meetup_video.bin.sig'] |
| self.fw_path_test = os.path.join(self.fw_path_base, |
| self.fw_pkg_test) |
| self.fw_path_origin = os.path.join(self.fw_path_base, |
| self.fw_pkg_origin) |
| self.fw_path_backup = os.path.join(self.fw_path_base, |
| self.fw_pkg_backup) |
| self.board = self.host.get_board().split(':')[1] |
| self.vid = cfm_usb_devices.LOGITECH_MEETUP.vendor_id |
| self.pid = cfm_usb_devices.LOGITECH_MEETUP.product_id |
| self.org_fw_ver = self.get_image_fw_ver() |
| |
| def cleanup(self): |
| """ |
| Cleanups after tests. |
| |
| Removes the test firmware. |
| Restores the original firmware files. |
| Flashes the camera to original firmware if needed. |
| """ |
| |
| # Delete test firmware package. |
| cmd = 'rm -rf {}'.format(self.fw_path_test) |
| self.host.run(cmd) |
| |
| # Delete the symlink created. |
| cmd = 'rm {}'.format(self.fw_path_origin) |
| self.host.run(cmd) |
| |
| # Move the backup package back. |
| cmd = 'mv {} {}'.format(self.fw_path_backup, self.fw_path_origin) |
| self.host.run(cmd) |
| |
| # Do not leave the camera with test (older) firmware. |
| if not self.is_device_firmware_equal_to(self.org_fw_ver): |
| logging.debug('Meetup device has old firmware after test' |
| 'Flashing new firmware') |
| self.flash_fw() |
| |
| super(enterprise_CFM_LogitechMeetupUpdater, self).cleanup() |
| |
| def _run_cmd(self, command, ignore_status=True): |
| """ |
| Runs command line on DUT, wait for completion and return the output. |
| |
| @param command: command line to run in dut. |
| @param ignore_status: if true ignore the status return by command |
| |
| @returns the command output |
| |
| """ |
| |
| logging.debug('Execute: %s', command) |
| |
| result = self.host.run(command, ignore_status=ignore_status) |
| if result.stderr: |
| output = result.stderr |
| else: |
| output = result.stdout |
| logging.debug('Output: %s', output) |
| return output |
| |
| def make_rootfs_writable(self): |
| """Checks and makes root filesystem writable.""" |
| |
| if not self.is_filesystem_readwrite(): |
| logging.info('DUT root file system is not writable. ' |
| 'Converting it writable...') |
| self.convert_rootfs_writable() |
| else: |
| logging.info('DUT root file system is writable.') |
| |
| def convert_rootfs_writable(self): |
| """Makes DUT rootfs writable.""" |
| |
| logging.info('Disabling rootfs verification...') |
| self.remove_rootfs_verification() |
| |
| logging.info('Rebooting...') |
| self.host.reboot() |
| |
| logging.info('Remounting..') |
| cmd = 'mount -o remount,rw /' |
| self.host.run(cmd) |
| |
| def remove_rootfs_verification(self): |
| """Removes rootfs verification.""" |
| |
| # 2 & 4 are default partitions, and the system boots from one of them. |
| # Code from chromite/scripts/deploy_chrome.py |
| KERNEL_A_PARTITION = 2 |
| KERNEL_B_PARTITION = 4 |
| |
| cmd_template = ('/usr/share/vboot/bin/make_dev_ssd.sh' |
| ' --partitions "%d %d"' |
| ' --remove_rootfs_verification --force') |
| cmd = cmd_template % (KERNEL_A_PARTITION, KERNEL_B_PARTITION) |
| self.host.run(cmd) |
| |
| def is_filesystem_readwrite(self): |
| """Checks if the root file system is writable.""" |
| |
| # Query the DUT's filesystem /dev/root and check whether it is rw |
| |
| cmd = 'cat /proc/mounts | grep "/dev/root"' |
| result = self._run_cmd(cmd) |
| fields = re.split(' |,', result) |
| |
| # Result of grep will be of the following format |
| # /dev/root / ext2 ro,seclabel <....truncated...> => readonly |
| # /dev/root / ext2 rw,seclabel <....truncated...> => readwrite |
| is_writable = fields.__len__() >= 4 and fields[3] == 'rw' |
| return is_writable |
| |
| def fw_ver_from_output_str(self, cmd_output): |
| """ |
| Parse firmware version of logitech-updater output. |
| |
| logitech-updater output differs for image_version and device_version |
| This function finds the line which contains string "Meetup" and parses |
| succeding lines. Each line is split on spaces (after collapsing spaces) |
| and index 1 gives component name (ex. Eeprom) and index 3 gives the |
| firmware version (ex. 1.14) |
| The actual output is given below. |
| |
| logitech-updater --image_version |
| |
| [INFO:main.cc(105)] PTZ Pro 2 Versions: |
| [INFO:main.cc(59)] Video version: 2.0.175 |
| [INFO:main.cc(61)] Eeprom version: 1.6 |
| [INFO:main.cc(63)] Mcu2 version: 3.9 |
| |
| [INFO:main.cc(105)] MeetUp Versions: |
| [INFO:main.cc(59)] Video version: 1.0.197 |
| [INFO:main.cc(61)] Eeprom version: 1.14 |
| [INFO:main.cc(65)] Audio version: 1.0.239 |
| [INFO:main.cc(67)] Codec version: 8.0.216 |
| [INFO:main.cc(69)] BLE version: 1.0.121 |
| |
| logitech-updater --device_version |
| |
| [INFO:main.cc(88)] Device name: Logitech MeetUp |
| [INFO:main.cc(59)] Video version: 1.0.197 |
| [INFO:main.cc(61)] Eeprom version: 1.14 |
| [INFO:main.cc(65)] Audio version: 1.0.239 |
| [INFO:main.cc(67)] Codec version: 8.0.216 |
| [INFO:main.cc(69)] BLE version: 1.0.121 |
| |
| |
| """ |
| |
| logging.debug('Parsing output from updater %s', cmd_output) |
| if 'MeetUp image not found' in cmd_output or 'MeetUp' not in cmd_output: |
| raise error.TestFail('MeetUp image not found on DUT') |
| try: |
| version = {} |
| output = cmd_output.split('\n') |
| start_line = -1 |
| |
| # Find the line of the output with string "Meetup |
| for i, l in enumerate(output): |
| if 'MeetUp' in l: |
| start_line = i |
| break |
| |
| if start_line == -1: |
| raise error.TestFail('Meetup version not found' |
| ' in updater output') |
| |
| output = output[start_line+1:start_line+6] |
| logging.debug('Parsing Meetup firmware info %s', str(output)) |
| for l in output: |
| |
| # Output lines are of the format |
| # [INFO:main.cc(59)] Video version: 1.0.197 |
| l = ' '.join(l.split()) # Collapse multiple spaces to one space |
| parts = l.split(' ') # parts[1] is "Video" parts[3] is 1.0.197 |
| version[parts[1]] = parts[3] |
| logging.debug('Version is %s', str(version)) |
| return version |
| except: |
| logging.error('Error while parsing logitech-updater output') |
| raise |
| |
| def get_updater_output(self, cmd): |
| """Get updater output while avoiding transient failures.""" |
| |
| NUM_RETRIES = 3 |
| WAIT_TIME = 5 |
| for _ in range(NUM_RETRIES): |
| output = self._run_cmd(cmd) |
| if 'Failed to read' in output: |
| time.sleep(WAIT_TIME) |
| continue |
| return output |
| |
| def get_image_fw_ver(self): |
| """Get the version of firmware on DUT.""" |
| |
| output = self.get_updater_output('logitech-updater --image_version' |
| ' --log_to=stdout') |
| return self.fw_ver_from_output_str(output) |
| |
| def get_device_fw_ver(self): |
| """Get the version of firmware on Meetup device.""" |
| |
| output = self.get_updater_output('logitech-updater --device_version' |
| ' --log_to=stdout') |
| return self.fw_ver_from_output_str(output) |
| |
| def copy_test_firmware(self): |
| """Copy test firmware from server to DUT.""" |
| |
| current_dir = os.path.dirname(os.path.realpath(__file__)) |
| src_firmware_path = os.path.join(current_dir, self.fw_pkg_test) |
| dst_firmware_path = self.fw_path_base |
| logging.info('Copy firmware from (%s) to (%s).', src_firmware_path, |
| dst_firmware_path) |
| self.host.send_file(src_firmware_path, dst_firmware_path, |
| delete_dest=True) |
| |
| def trigger_updater(self): |
| """Trigger udev rule to run fw updater by power cycling the usb.""" |
| |
| try: |
| power_cycle_usb_util.power_cycle_usb_vidpid(self.host, self.board, |
| self.vid, self.pid) |
| except KeyError: |
| raise error.TestFail('Counld\'t find target device: ' |
| 'vid:pid {}:{}'.format(self.vid, self.pid)) |
| |
| def wait_for_meetup_device(self): |
| """ |
| Wait for Meetup device device to be enumerated. |
| |
| Check if a device with given (vid,pid) is present. |
| Timeout after wait_time seconds. Default 30 seconds |
| """ |
| |
| TIME_SLEEP = 10 |
| NUM_ITERATIONS = 3 |
| WAIT_TIME = TIME_SLEEP * NUM_ITERATIONS |
| |
| logging.debug('Waiting for Meetup device') |
| for _ in range(NUM_ITERATIONS): |
| res = power_cycle_usb_util.get_port_number_from_vidpid( |
| self.host, self.vid, self.pid) |
| (bus_num, port_num) = res |
| if bus_num is not None and port_num is not None: |
| logging.debug('Meetup device detected') |
| return |
| else: |
| logging.debug('Meetup device not detected.' |
| 'Waiting for (%s) seconds', TIME_SLEEP) |
| time.sleep(TIME_SLEEP) |
| |
| logging.error('Unable to detect the device after (%s) seconds.' |
| 'Timing out...', WAIT_TIME) |
| raise error.TestFail('Target device not detected.') |
| |
| def setup_fw(self, firmware_package): |
| """Setup firmware package that is going to be used for updating.""" |
| |
| firmware_path = os.path.join(self.fw_path_base, firmware_package) |
| cmd = 'ln -sfn {} {}'.format(firmware_path, self.fw_path_origin) |
| self.host.run(cmd) |
| |
| def flash_fw(self, force=False): |
| """Flash certain firmware to device. |
| |
| Run logitech firmware updater on DUT to flash the firmware setuped |
| to target device (PTZ Pro 2). |
| |
| @param force: run with force update, will bypass fw version check. |
| |
| """ |
| |
| cmd = ('/usr/sbin/logitech-updater --log_to=stdout --update_components' |
| ' --lock') |
| if force: |
| cmd += ' --force' |
| output = self._run_cmd(cmd) |
| return output |
| |
| def print_fw_version(self, version, info_str=''): |
| """Pretty print Meetup firmware version.""" |
| |
| if info_str: |
| print info_str |
| print 'Video version: ', version['Video'] |
| print 'Eeprom version: ', version['Eeprom'] |
| print 'Audio version: ', version['Audio'] |
| print 'Codec version: ', version['Codec'] |
| print 'BLE version: ', version['BLE'] |
| |
| def is_device_firmware_equal_to(self, expected_ver): |
| """Check that the device fw version is equal to given version.""" |
| |
| device_fw_version = self.get_device_fw_ver() |
| if device_fw_version != expected_ver: |
| logging.error('Device firmware version is not the expected version') |
| self.print_fw_version(device_fw_version, 'Device firmware version') |
| self.print_fw_version(expected_ver, 'Expected firmware version') |
| return False |
| else: |
| return True |
| |
| def flash_old_firmware(self): |
| """Flash old (test) version of firmware on the device.""" |
| |
| # Flash old FW to device. |
| self.setup_fw(self.fw_pkg_test) |
| test_fw_ver = self.get_image_fw_ver() |
| self.print_fw_version(test_fw_ver, 'Test firmware version') |
| output = self.flash_fw(force=True) |
| time.sleep(POWER_CYCLE_WAIT_TIME_SEC) |
| with open(self.log_file, 'w') as f: |
| delim = '-' * 8 |
| f.write('{}Log info for writing old firmware{}' |
| '\n'.format(delim, delim)) |
| f.write(output) |
| if not self.is_device_firmware_equal_to(test_fw_ver): |
| raise error.TestFail('Flashing old firmware failed') |
| logging.info('Device flashed with test firmware') |
| |
| def backup_original_firmware(self): |
| """Backup existing firmware on DUT.""" |
| # Copy old FW to device. |
| cmd = 'mv {} {}'.format(self.fw_path_origin, self.fw_path_backup) |
| self.host.run(cmd) |
| |
| def is_updater_running(self): |
| """Checks if the logitech-updater is running.""" |
| |
| cmd = 'logitech-updater --lock --device_version --log_to=stdout' |
| output = self._run_cmd(cmd) |
| return 'There is another logitech-updater running' in output |
| |
| def wait_for_updater(self): |
| """Wait logitech-updater to stop or timeout after 6 minutes.""" |
| |
| NUM_ITERATION = 12 |
| WAIT_TIME = 30 # seconds |
| logging.debug('Wait for any currently running updater to finish') |
| for _ in range(NUM_ITERATION): |
| if self.is_updater_running(): |
| logging.debug('logitech-updater is running.' |
| 'Waiting for 30 seconds') |
| time.sleep(WAIT_TIME) |
| else: |
| logging.debug('logitech-updater not running') |
| return |
| logging.error('logitech-updater is still running after 6 minutes') |
| |
| def test_firmware_update(self): |
| """Trigger firmware updater and check device firmware version.""" |
| |
| # Simulate hotplug to run FW updater. |
| logging.info('Setup original firmware') |
| self.setup_fw(self.fw_pkg_backup) |
| logging.info('Simulate hot plugging the device') |
| self.trigger_updater() |
| self.wait_for_meetup_device() |
| |
| # The firmware check will fail if the check runs in a short window |
| # between the device being detected and the firmware updater starting. |
| # Adding a delay to reduce the chance of that scenerio. |
| time.sleep(POWER_CYCLE_WAIT_TIME_SEC) |
| |
| self.wait_for_updater() |
| |
| if not self.is_device_firmware_equal_to(self.org_fw_ver): |
| raise error.TestFail('Camera not updated to new firmware') |
| logging.info('Firmware update was completed successfully') |
| |
| def run_once(self): |
| """ |
| Entry point for test. |
| |
| The following actions are performed in this test. |
| - Device is flashed with older firmware. |
| - Powercycle usb port to simulate hotplug inorder to start the updater. |
| - Check that the device is updated with newer firmware. |
| """ |
| |
| # Check if updater is already running |
| self.wait_for_updater() |
| |
| self.print_fw_version(self.org_fw_ver, |
| 'Original firmware version on DUT') |
| self.print_fw_version(self.get_device_fw_ver(), |
| 'Firmware version on Meetup device') |
| |
| self.make_rootfs_writable() |
| self.backup_original_firmware() |
| |
| # Flash test firmware version |
| self.copy_test_firmware() |
| self.flash_old_firmware() |
| |
| # Test firmware update |
| self.test_firmware_update() |
| logging.info('Logitech Meetup firmware updater test was successful') |