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 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#           }
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'
82SYMBOLIC_BID_LENGTH = 4
83
84usb_update = argparse.ArgumentParser()
85usb_update.add_argument('-a', '--any', dest='universal', action='store_true')
86# use /dev/tpm0 to send the command
87usb_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.
91usb_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
94usb_update.add_argument('-u', '--upstart', '-p', '--post_reset',
95                        dest='post_reset', action='store_true')
96usb_update.add_argument('extras', nargs=argparse.REMAINDER)
97
98
99def 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
115def 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
128def 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
158def 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
185def 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
202def 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
218def 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
243def StopTrunksd(client):
244    """Stop trunksd on the client"""
245    if 'running' in client.run('status trunksd').stdout:
246        client.run('stop trunksd')
247
248
249def 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
284def 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
290def GetFwVersion(client):
291    """Get the running version using 'usb_updater --fwver'"""
292    return GetVersionFromUpdater(client, ['--fwver', '-a'])
293
294
295def GetBinVersion(client, image=CR50_PROD):
296    """Get the image version using 'usb_updater --binvers image'"""
297    return GetVersionFromUpdater(client, ['--binvers', image])
298
299
300def 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
306def 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
332def 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
357def 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
389def 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
414def HasPrepvtImage(client):
415    """Returns True if cr50.bin.prepvt exists on the dut"""
416    return client.path_exists(CR50_PREPVT)
417
418
419def 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
433def 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
453def 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
471def 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
507def 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
531def 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
546def 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
564def 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
576def 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
601def 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
627def 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