• 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
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