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