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