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