Home | History | Annotate | Download | only in cros
      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 
      5 import argparse
      6 import logging
      7 import re
      8 
      9 from autotest_lib.client.common_lib import error
     10 
     11 
     12 RO = 'ro'
     13 RW = 'rw'
     14 BID = 'bid'
     15 CR50_PROD = '/opt/google/cr50/firmware/cr50.bin.prod'
     16 CR50_PREPVT = '/opt/google/cr50/firmware/cr50.bin.prepvt'
     17 CR50_STATE = '/var/cache/cr50*'
     18 CR50_VERSION = '/var/cache/cr50-version'
     19 GET_CR50_VERSION = 'cat %s' % CR50_VERSION
     20 GET_CR50_MESSAGES ='grep "cr50-.*\[" /var/log/messages'
     21 UPDATE_FAILURE = 'unexpected cr50-update exit code'
     22 DUMMY_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 #           }
     67 VERSION_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 }
     74 UPDATE_TIMEOUT = 60
     75 UPDATE_OK = 1
     76 
     77 ERASED_BID_INT = 0xffffffff
     78 # With an erased bid, the flags and board id will both be erased
     79 ERASED_CHIP_BID = (ERASED_BID_INT, ERASED_BID_INT, ERASED_BID_INT)
     80 # Any image with this board id will run on any device
     81 EMPTY_IMAGE_BID = '00000000:00000000:00000000'
     82 EMPTY_IMAGE_BID_CHARACTERS = set(EMPTY_IMAGE_BID)
     83 SYMBOLIC_BID_LENGTH = 4
     84 
     85 gsctool = argparse.ArgumentParser()
     86 gsctool.add_argument('-a', '--any', dest='universal', action='store_true')
     87 # use /dev/tpm0 to send the command
     88 gsctool.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.
     93 gsctool.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
     97 gsctool.add_argument('-u', '--upstart', '-p', '--post_reset', dest='post_reset',
     98                      action='store_true')
     99 gsctool.add_argument('extras', nargs=argparse.REMAINDER)
    100 
    101 
    102 def 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 
    118 def 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 
    131 def 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 
    161 def 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 
    187 def 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 
    204 def 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 
    220 def 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 
    245 def StopTrunksd(client):
    246     """Stop trunksd on the client"""
    247     if 'running' in client.run('status trunksd').stdout:
    248         client.run('stop trunksd')
    249 
    250 
    251 def 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 
    287 def GetVersionFromUpdater(client, args):
    288     """Return the version from gsctool"""
    289     result = GSCTool(client, args).stdout.strip()
    290     return FindVersion(result, args[0])
    291 
    292 
    293 def GetFwVersion(client):
    294     """Get the running version using 'gsctool --fwver'"""
    295     return GetVersionFromUpdater(client, ['--fwver', '-a'])
    296 
    297 
    298 def GetBinVersion(client, image=CR50_PROD):
    299     """Get the image version using 'gsctool --binvers image'"""
    300     return GetVersionFromUpdater(client, ['--binvers', image])
    301 
    302 
    303 def 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 
    309 def 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 
    335 def 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 
    360 def 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 
    392 def 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 
    417 def HasPrepvtImage(client):
    418     """Returns True if cr50.bin.prepvt exists on the dut"""
    419     return client.path_exists(CR50_PREPVT)
    420 
    421 
    422 def 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 
    436 def 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 
    456 def 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 
    475 def 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 
    509 def 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 
    533 def 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 
    548 def 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 
    566 def 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 
    578 def RMAOpen(client, cmd='', ignore_status=False):
    579     """Run gsctool RMA commands"""
    580     return GSCTool(client, ['-a', '-r', cmd], ignore_status)
    581 
    582 
    583 def 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 
    608 def 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 
    634 def 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