1# Copyright 2017 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 argparse 6import logging 7import re 8 9from autotest_lib.client.common_lib import error 10 11 12RO = 'ro' 13RW = 'rw' 14BID = 'bid' 15CR50_PROD = '/opt/google/cr50/firmware/cr50.bin.prod' 16CR50_PREPVT = '/opt/google/cr50/firmware/cr50.bin.prepvt' 17CR50_STATE = '/var/cache/cr50*' 18CR50_VERSION = '/var/cache/cr50-version' 19GET_CR50_VERSION = 'cat %s' % CR50_VERSION 20GET_CR50_MESSAGES ='grep "cr50-.*\[" /var/log/messages' 21UPDATE_FAILURE = 'unexpected cr50-update exit code' 22DUMMY_VER = '-1.-1.-1' 23# This dictionary is used to search the gsctool output for the version strings. 24# There are two gsctool commands that will return versions: 'fwver' and 25# 'binvers'. 26# 27# 'fwver' is used to get the running RO and RW versions from cr50 28# 'binvers' gets the version strings for each RO and RW region in the given 29# file 30# 31# The value in the dictionary is the regular expression that can be used to 32# find the version strings for each region. 33# 34# --fwver 35# example output: 36# open_device 18d1:5014 37# found interface 3 endpoint 4, chunk_len 64 38# READY 39# ------- 40# start 41# target running protocol version 6 42# keyids: RO 0xaa66150f, RW 0xde88588d 43# offsets: backup RO at 0x40000, backup RW at 0x44000 44# Current versions: 45# RO 0.0.10 46# RW 0.0.21 47# match groupdict: 48# { 49# 'ro': '0.0.10', 50# 'rw': '0.0.21' 51# } 52# 53# --binvers 54# example output: 55# read 524288(0x80000) bytes from /tmp/cr50.bin 56# RO_A:0.0.10 RW_A:0.0.21[00000000:00000000:00000000] 57# RO_B:0.0.10 RW_B:0.0.21[00000000:00000000:00000000] 58# match groupdict: 59# { 60# 'rw_b': '0.0.21', 61# 'rw_a': '0.0.21', 62# 'ro_b': '0.0.10', 63# 'ro_a': '0.0.10', 64# 'bid_a': '00000000:00000000:00000000', 65# 'bid_b': '00000000:00000000:00000000' 66# } 67VERSION_RE = { 68 '--fwver' : '\nRO (?P<ro>\S+).*\nRW (?P<rw>\S+)', 69 '--binvers' : 'RO_A:(?P<ro_a>[\d\.]+).*' \ 70 'RW_A:(?P<rw_a>[\d\.]+)(\[(?P<bid_a>[\d\:A-z]+)\])?.*' \ 71 'RO_B:(?P<ro_b>\S+).*' \ 72 'RW_B:(?P<rw_b>[\d\.]+)(\[(?P<bid_b>[\d\:A-z]+)\])?.*', 73} 74UPDATE_TIMEOUT = 60 75UPDATE_OK = 1 76 77ERASED_BID_INT = 0xffffffff 78# With an erased bid, the flags and board id will both be erased 79ERASED_CHIP_BID = (ERASED_BID_INT, ERASED_BID_INT, ERASED_BID_INT) 80# Any image with this board id will run on any device 81EMPTY_IMAGE_BID = '00000000:00000000:00000000' 82EMPTY_IMAGE_BID_CHARACTERS = set(EMPTY_IMAGE_BID) 83SYMBOLIC_BID_LENGTH = 4 84 85gsctool = argparse.ArgumentParser() 86gsctool.add_argument('-a', '--any', dest='universal', action='store_true') 87# use /dev/tpm0 to send the command 88gsctool.add_argument('-s', '--systemdev', dest='systemdev', action='store_true') 89# Any command used for something other than updating. These commands should 90# never timeout because they forced cr50 to reboot. They should all just 91# return information about cr50 and should only have a nonzero exit status if 92# something went wrong. 93gsctool.add_argument('-b', '--binvers', '-f', '--fwver', '-i', '--board_id', 94 '-r', '--rma_auth', '-F', '--factory', '-m', '--tpm_mode', 95 dest='info_cmd', action='store_true') 96# upstart and post_reset will post resets instead of rebooting immediately 97gsctool.add_argument('-u', '--upstart', '-p', '--post_reset', dest='post_reset', 98 action='store_true') 99gsctool.add_argument('extras', nargs=argparse.REMAINDER) 100 101 102def AssertVersionsAreEqual(name_a, ver_a, name_b, ver_b): 103 """Raise an error ver_a isn't the same as ver_b 104 105 Args: 106 name_a: the name of section a 107 ver_a: the version string for section a 108 name_b: the name of section b 109 ver_b: the version string for section b 110 111 Raises: 112 AssertionError if ver_a is not equal to ver_b 113 """ 114 assert ver_a == ver_b, ('Versions do not match: %s %s %s %s' % 115 (name_a, ver_a, name_b, ver_b)) 116 117 118def GetNewestVersion(ver_a, ver_b): 119 """Compare the versions. Return the newest one. If they are the same return 120 None.""" 121 a = [int(x) for x in ver_a.split('.')] 122 b = [int(x) for x in ver_b.split('.')] 123 124 if a > b: 125 return ver_a 126 if b > a: 127 return ver_b 128 return None 129 130 131def GetVersion(versions, name): 132 """Return the version string from the dictionary. 133 134 Get the version for each key in the versions dictionary that contains the 135 substring name. Make sure all of the versions match and return the version 136 string. Raise an error if the versions don't match. 137 138 Args: 139 version: dictionary with the partition names as keys and the 140 partition version strings as values. 141 name: the string used to find the relevant items in versions. 142 143 Returns: 144 the version from versions or "-1.-1.-1" if an invalid RO was detected. 145 """ 146 ver = None 147 key = None 148 for k, v in versions.iteritems(): 149 if name in k: 150 if v == DUMMY_VER: 151 logging.info('Detected invalid %s %s', name, v) 152 return v 153 elif ver: 154 AssertVersionsAreEqual(key, ver, k, v) 155 else: 156 ver = v 157 key = k 158 return ver 159 160 161def FindVersion(output, arg): 162 """Find the ro and rw versions. 163 164 Args: 165 output: The string to search 166 arg: string representing the gsctool option, either '--binvers' or 167 '--fwver' 168 169 Returns: 170 a tuple of the ro and rw versions 171 """ 172 versions = re.search(VERSION_RE[arg], output) 173 if not versions: 174 raise Exception('Unable to determine version from: %s' % output) 175 176 versions = versions.groupdict() 177 ro = GetVersion(versions, RO) 178 rw = GetVersion(versions, RW) 179 # --binver is the only gsctool command that may have bid keys in its 180 # versions dictionary. If no bid keys exist, bid will be None. 181 bid = GetVersion(versions, BID) 182 # Use GetBoardIdInfoString to standardize all board ids to the non 183 # symbolic form. 184 return ro, rw, GetBoardIdInfoString(bid, symbolic=False) 185 186 187def GetSavedVersion(client): 188 """Return the saved version from /var/cache/cr50-version 189 190 Some boards dont have cr50.bin.prepvt. They may still have prepvt flags. 191 It is possible that cr50-update wont successfully run in this case. 192 Return None if the file doesn't exist. 193 194 Returns: 195 the version saved in cr50-version or None if cr50-version doesn't exist 196 """ 197 if not client.path_exists(CR50_VERSION): 198 return None 199 200 result = client.run(GET_CR50_VERSION).stdout.strip() 201 return FindVersion(result, '--fwver') 202 203 204def GetRLZ(client): 205 """Get the RLZ brand code from vpd. 206 207 Args: 208 client: the object to run commands on 209 210 Returns: 211 The current RLZ code or '' if the space doesn't exist 212 """ 213 result = client.run('vpd -g rlz_brand_code', ignore_status=True) 214 if (result.exit_status and (result.exit_status != 3 or 215 "Vpd data 'rlz_brand_code' was not found." not in result.stderr)): 216 raise error.TestFail(result) 217 return result.stdout.strip() 218 219 220def SetRLZ(client, rlz): 221 """Set the RLZ brand code in vpd 222 223 Args: 224 client: the object to run commands on 225 rlz: 4 character string. 226 227 Raises: 228 TestError if the RLZ code is too long or if setting the code failed. 229 """ 230 rlz = rlz.strip() 231 if len(rlz) > SYMBOLIC_BID_LENGTH: 232 raise error.TestError('RLZ is too long. Use a max of 4 characters') 233 234 if rlz == GetRLZ(client): 235 return 236 elif rlz: 237 client.run('vpd -s rlz_brand_code=%s' % rlz) 238 else: 239 client.run('vpd -d rlz_brand_code') 240 241 if rlz != GetRLZ(client): 242 raise error.TestError('Could not set RLZ code') 243 244 245def StopTrunksd(client): 246 """Stop trunksd on the client""" 247 if 'running' in client.run('status trunksd').stdout: 248 client.run('stop trunksd') 249 250 251def GSCTool(client, args, ignore_status=False): 252 """Run gsctool with the given args. 253 254 Args: 255 client: the object to run commands on 256 args: a list of strings that contiain the gsctool args 257 258 Returns: 259 the result of gsctool 260 """ 261 options = gsctool.parse_args(args) 262 263 if options.systemdev: 264 StopTrunksd(client) 265 266 # If we are updating the cr50 image, gsctool will return a non-zero exit 267 # status so we should ignore it. 268 ignore_status = not options.info_cmd or ignore_status 269 # immediate reboots are only honored if the command is sent using /dev/tpm0 270 expect_reboot = ((options.systemdev or options.universal) and 271 not options.post_reset and not options.info_cmd) 272 273 result = client.run('gsctool %s' % ' '.join(args), 274 ignore_status=ignore_status, 275 ignore_timeout=expect_reboot, 276 timeout=UPDATE_TIMEOUT) 277 278 # After a posted reboot, the gsctool exit code should equal 1. 279 if (result and result.exit_status and result.exit_status != UPDATE_OK and 280 not ignore_status): 281 logging.debug(result) 282 raise error.TestFail('Unexpected gsctool exit code after %s %d' % 283 (' '.join(args), result.exit_status)) 284 return result 285 286 287def GetVersionFromUpdater(client, args): 288 """Return the version from gsctool""" 289 result = GSCTool(client, args).stdout.strip() 290 return FindVersion(result, args[0]) 291 292 293def GetFwVersion(client): 294 """Get the running version using 'gsctool --fwver'""" 295 return GetVersionFromUpdater(client, ['--fwver', '-a']) 296 297 298def GetBinVersion(client, image=CR50_PROD): 299 """Get the image version using 'gsctool --binvers image'""" 300 return GetVersionFromUpdater(client, ['--binvers', image]) 301 302 303def GetVersionString(ver): 304 """Combine the RO and RW tuple into a understandable string""" 305 return 'RO %s RW %s%s' % (ver[0], ver[1], 306 ' BID %s' % ver[2] if ver[2] else '') 307 308 309def GetRunningVersion(client): 310 """Get the running Cr50 version. 311 312 The version from gsctool and /var/cache/cr50-version should be the 313 same. Get both versions and make sure they match. 314 315 Args: 316 client: the object to run commands on 317 318 Returns: 319 running_ver: a tuple with the ro and rw version strings 320 321 Raises: 322 TestFail 323 - If the version in /var/cache/cr50-version is not the same as the 324 version from 'gsctool --fwver' 325 """ 326 running_ver = GetFwVersion(client) 327 saved_ver = GetSavedVersion(client) 328 329 if saved_ver: 330 AssertVersionsAreEqual('Running', GetVersionString(running_ver), 331 'Saved', GetVersionString(saved_ver)) 332 return running_ver 333 334 335def GetActiveCr50ImagePath(client): 336 """Get the path the device uses to update cr50 337 338 Extract the active cr50 path from the cr50-update messages. This path is 339 determined by cr50-get-name based on the board id flag value. 340 341 Args: 342 client: the object to run commands on 343 344 Raises: 345 TestFail 346 - If cr50-update uses more than one path or if the path we find 347 is not a known cr50 update path. 348 """ 349 ClearUpdateStateAndReboot(client) 350 messages = client.run(GET_CR50_MESSAGES).stdout.strip() 351 paths = set(re.findall('/opt/google/cr50/firmware/cr50.bin[\S]+', messages)) 352 if not paths: 353 raise error.TestFail('Could not determine cr50-update path') 354 path = paths.pop() 355 if len(paths) > 1 or (path != CR50_PROD and path != CR50_PREPVT): 356 raise error.TestFail('cannot determine cr50 path') 357 return path 358 359 360def CheckForFailures(client, last_message): 361 """Check for any unexpected cr50-update exit codes. 362 363 This only checks the cr50 update messages that have happened since 364 last_message. If a unexpected exit code is detected it will raise an error> 365 366 Args: 367 client: the object to run commands on 368 last_message: the last cr50 message from the last update run 369 370 Returns: 371 the last cr50 message in /var/log/messages 372 373 Raises: 374 TestFail 375 - If there is a unexpected cr50-update exit code after last_message 376 in /var/log/messages 377 """ 378 messages = client.run(GET_CR50_MESSAGES).stdout.strip() 379 if last_message: 380 messages = messages.rsplit(last_message, 1)[-1].split('\n') 381 failures = [] 382 for message in messages: 383 if UPDATE_FAILURE in message: 384 failures.append(message) 385 if len(failures): 386 logging.info(messages) 387 raise error.TestFail('Detected unexpected exit code during update: ' 388 '%s' % failures) 389 return messages[-1] 390 391 392def VerifyUpdate(client, ver='', last_message=''): 393 """Verify that the saved update state is correct and there were no 394 unexpected cr50-update exit codes since the last update. 395 396 Args: 397 client: the object to run commands on 398 ver: the expected version tuple (ro ver, rw ver) 399 last_message: the last cr50 message from the last update run 400 401 Returns: 402 new_ver: a tuple containing the running ro and rw versions 403 last_message: The last cr50 update message in /var/log/messages 404 """ 405 # Check that there were no unexpected reboots from cr50-result 406 last_message = CheckForFailures(client, last_message) 407 logging.debug('last cr50 message %s', last_message) 408 409 new_ver = GetRunningVersion(client) 410 if ver != '': 411 if DUMMY_VER != ver[0]: 412 AssertVersionsAreEqual('Old RO', ver[0], 'Updated RO', new_ver[0]) 413 AssertVersionsAreEqual('Old RW', ver[1], 'Updated RW', new_ver[1]) 414 return new_ver, last_message 415 416 417def HasPrepvtImage(client): 418 """Returns True if cr50.bin.prepvt exists on the dut""" 419 return client.path_exists(CR50_PREPVT) 420 421 422def ClearUpdateStateAndReboot(client): 423 """Removes the cr50 status files in /var/cache and reboots the AP""" 424 # If any /var/cache/cr50* files exist, remove them. 425 result = client.run('ls %s' % CR50_STATE, ignore_status=True) 426 if not result.exit_status: 427 client.run('rm %s' % ' '.join(result.stdout.split())) 428 elif result.exit_status != 2: 429 # Exit status 2 means the file didn't exist. If the command fails for 430 # some other reason, raise an error. 431 logging.debug(result) 432 raise error.TestFail(result.stderr) 433 client.reboot() 434 435 436def InstallImage(client, src, dest=CR50_PROD): 437 """Copy the image at src to dest on the dut 438 439 Args: 440 client: the object to run commands on 441 src: the image location of the server 442 dest: the desired location on the dut 443 444 Returns: 445 The filename where the image was copied to on the dut, a tuple 446 containing the RO and RW version of the file 447 """ 448 # Send the file to the DUT 449 client.send_file(src, dest) 450 451 ver = GetBinVersion(client, dest) 452 client.run('sync') 453 return dest, ver 454 455 456def GetBoardIdInfoTuple(board_id_str): 457 """Convert the string into board id args. 458 459 Split the board id string board_id:(mask|board_id_inv):flags to a tuple of 460 its parts. Each element will be converted to an integer. 461 462 Returns: 463 board id int, mask|board_id_inv, and flags or None if its a universal 464 image. 465 """ 466 # In tests None is used for universal board ids. Some old images don't 467 # support getting board id, so we use None. Convert 0:0:0 to None. 468 if not board_id_str or set(board_id_str) == EMPTY_IMAGE_BID_CHARACTERS: 469 return None 470 471 board_id, param2, flags = board_id_str.split(':') 472 return GetIntBoardId(board_id), int(param2, 16), int(flags, 16) 473 474 475def GetBoardIdInfoString(board_id_info, symbolic=False): 476 """Convert the board id list or str into a symbolic or non symbolic str. 477 478 This can be used to convert the board id info list into a symbolic or non 479 symbolic board id string. It can also be used to convert a the board id 480 string into a board id string with a symbolic or non symbolic board id 481 482 Args: 483 board_id_info: A string of the form board_id:(mask|board_id_inv):flags 484 or a list with the board_id, (mask|board_id_inv), flags 485 486 Returns: 487 (board_id|symbolic_board_id):(mask|board_id_inv):flags. Will return 488 None if if the given board id info is empty or is not valid 489 """ 490 # Convert board_id_info to a tuple if it's a string. 491 if isinstance(board_id_info, str): 492 board_id_info = GetBoardIdInfoTuple(board_id_info) 493 494 if not board_id_info: 495 return None 496 497 board_id, param2, flags = board_id_info 498 # Get the hex string for board id 499 board_id = '%08x' % GetIntBoardId(board_id) 500 501 # Convert the board id hex to a symbolic board id 502 if symbolic: 503 board_id = GetSymbolicBoardId(board_id) 504 505 # Return the board_id_str:8_digit_hex_mask: 8_digit_hex_flags 506 return '%s:%08x:%08x' % (board_id, param2, flags) 507 508 509def GetSymbolicBoardId(board_id): 510 """Convert an integer board id to a symbolic string 511 512 Args: 513 board_id: the board id to convert to the symbolic board id 514 515 Returns: 516 the 4 character symbolic board id 517 """ 518 symbolic_board_id = '' 519 board_id = GetIntBoardId(board_id) 520 521 # Convert the int to a symbolic board id 522 for i in range(SYMBOLIC_BID_LENGTH): 523 symbolic_board_id += chr((board_id >> (i * 8)) & 0xff) 524 symbolic_board_id = symbolic_board_id[::-1] 525 526 # Verify the created board id is 4 characters 527 if len(symbolic_board_id) != SYMBOLIC_BID_LENGTH: 528 raise error.TestFail('Created invalid symbolic board id %s' % 529 symbolic_board_id) 530 return symbolic_board_id 531 532 533def ConvertSymbolicBoardId(symbolic_board_id): 534 """Convert the symbolic board id str to an int 535 536 Args: 537 symbolic_board_id: a ASCII string. It can be up to 4 characters 538 539 Returns: 540 the symbolic board id string converted to an int 541 """ 542 board_id = 0 543 for c in symbolic_board_id: 544 board_id = ord(c) | (board_id << 8) 545 return board_id 546 547 548def GetIntBoardId(board_id): 549 """"Return the gsctool interpretation of board_id 550 551 Args: 552 board_id: a int or string value of the board id 553 554 Returns: 555 a int representation of the board id 556 """ 557 if type(board_id) == int: 558 return board_id 559 560 if len(board_id) <= SYMBOLIC_BID_LENGTH: 561 return ConvertSymbolicBoardId(board_id) 562 563 return int(board_id, 16) 564 565 566def GetExpectedFlags(flags): 567 """If flags are not specified, gsctool will set them to 0xff00 568 569 Args: 570 flags: The int value or None 571 572 Returns: 573 the original flags or 0xff00 if flags is None 574 """ 575 return flags if flags != None else 0xff00 576 577 578def RMAOpen(client, cmd='', ignore_status=False): 579 """Run gsctool RMA commands""" 580 return GSCTool(client, ['-a', '-r', cmd], ignore_status) 581 582 583def GetChipBoardId(client): 584 """Return the board id and flags 585 586 Args: 587 client: the object to run commands on 588 589 Returns: 590 a tuple with the int values of board id, board id inv, flags 591 592 Raises: 593 TestFail if the second board id response field is not ~board_id 594 """ 595 result = GSCTool(client, ['-a', '-i']).stdout.strip() 596 board_id_info = result.split('Board ID space: ')[-1].strip().split(':') 597 board_id, board_id_inv, flags = [int(val, 16) for val in board_id_info] 598 logging.info('BOARD_ID: %x:%x:%x', board_id, board_id_inv, flags) 599 600 if board_id == board_id_inv == flags == ERASED_BID_INT: 601 logging.info('board id is erased') 602 elif board_id & board_id_inv: 603 raise error.TestFail('board_id_inv should be ~board_id got %x %x' % 604 (board_id, board_id_inv)) 605 return board_id, board_id_inv, flags 606 607 608def CheckChipBoardId(client, board_id, flags): 609 """Compare the given board_id and flags to the running board_id and flags 610 611 Interpret board_id and flags how gsctool would interpret them, then compare 612 those interpreted values to the running board_id and flags. 613 614 Args: 615 client: the object to run commands on 616 board_id: a hex str, symbolic str, or int value for board_id 617 flags: the int value of flags or None 618 619 Raises: 620 TestFail if the new board id info does not match 621 """ 622 # Read back the board id and flags 623 new_board_id, _, new_flags = GetChipBoardId(client) 624 625 expected_board_id = GetIntBoardId(board_id) 626 expected_flags = GetExpectedFlags(flags) 627 628 if new_board_id != expected_board_id or new_flags != expected_flags: 629 raise error.TestFail('Failed to set board id expected %x:%x, but got ' 630 '%x:%x' % (expected_board_id, expected_flags, 631 new_board_id, new_flags)) 632 633 634def SetChipBoardId(client, board_id, flags=None): 635 """Sets the board id and flags 636 637 Args: 638 client: the object to run commands on 639 board_id: a string of the symbolic board id or board id hex value. If 640 the string is less than 4 characters long it will be 641 considered a symbolic value 642 flags: a int flag value. If board_id is a symbolic value, then this will 643 be ignored. 644 645 Raises: 646 TestFail if we were unable to set the flags to the correct value 647 """ 648 649 board_id_arg = board_id 650 if flags != None: 651 board_id_arg += ':' + hex(flags) 652 653 # Set the board id using the given board id and flags 654 result = GSCTool(client, ['-a', '-i', board_id_arg]).stdout.strip() 655 656 CheckChipBoardId(client, board_id, flags) 657