diff options
Diffstat (limited to 'extra/cr50_rma_open/cr50_rma_open.py')
-rwxr-xr-x | extra/cr50_rma_open/cr50_rma_open.py | 665 |
1 files changed, 0 insertions, 665 deletions
diff --git a/extra/cr50_rma_open/cr50_rma_open.py b/extra/cr50_rma_open/cr50_rma_open.py deleted file mode 100755 index 42ddbbac2d..0000000000 --- a/extra/cr50_rma_open/cr50_rma_open.py +++ /dev/null @@ -1,665 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright 2018 The Chromium OS Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -# Used to access the cr50 console and handle RMA Open -"""Open cr50 using RMA authentication. - -Run RMA Open to enable CCD on Cr50. The utility can be used to get a -url that will generate an authcode to open cr50. It can also be used to -try opening cr50 with the generated authcode. - -The last challenge is the only valid one, so don't generate a challenge -10 times and then use the first URL. You can only use the last one. - -For RMA Open: -Connect suzyq to the dut and your workstation. - -Check the basic setup with - sudo python cr50_rma_open.py -c - -If the setup is broken. Follow the debug print statements to try to fix -the error. Rerun until the script says Cr50 setup ok. - -After the setup is verified, run the following command to generate the -challenge url - sudo python cr50_rma_open.py -g -i $HWID - -Go to the URL from by that command to generate an authcode. Once you have -the authcode, you can use it to open cr50. - sudo python cr50_rma_open.py -a $AUTHCODE - -If for some reason hardware write protect doesn't get disabled during rma -open or gets enabled at some point the script can be used to disable -write protect. - sudo python cr50_rma_open.py -w - -When prepping devices for the testlab, you need to enable testlab mode. -Prod cr50 images can't enable testlab mode. If the device is running a -prod image, you can skip this step. - sudo python cr50_rma_open.py -t -""" - -import argparse -import glob -import logging -import re -import subprocess -import sys -import time - -import serial - -SCRIPT_VERSION = 5 -CCD_IS_UNRESTRICTED = 1 << 0 -WP_IS_DISABLED = 1 << 1 -TESTLAB_IS_ENABLED = 1 << 2 -RMA_OPENED = CCD_IS_UNRESTRICTED | WP_IS_DISABLED -URL = ('https://www.google.com/chromeos/partner/console/cr50reset?' - 'challenge=%s&hwid=%s') -RMA_SUPPORT_PROD = '0.3.3' -RMA_SUPPORT_PREPVT = '0.4.5' -DEV_MODE_OPEN_PROD = '0.3.9' -DEV_MODE_OPEN_PREPVT = '0.4.7' -TESTLAB_PROD = '0.3.10' -CR50_USB = '18d1:5014' -CR50_LSUSB_CMD = ['lsusb', '-vd', CR50_USB] -ERASED_BID = 'ffffffff' - -DEBUG_MISSING_USB = """ -Unable to find Cr50 Device 18d1:5014 - -DEBUG MISSING USB: - - Make sure suzyq is plugged into the correct DUT port - - Try flipping the cable - - unplug the cable for 5s then plug it back in -""" - -DEBUG_DEVICE = """ -DEBUG DEVICE COMMUNICATION: -Issues communicating with %s - -A 18d1:5014 device exists, so make sure you have selected the correct -/dev/ttyUSB -""" - -DEBUG_SERIALNAME = """ -DEBUG SERIALNAME: -Found the USB device, but can't match the usb serialname. Check the -serialname you passed into cr50_rma_open or try running without a -serialname. -""" - -DEBUG_CONNECTION = """ -DEBUG CONNECTION: -Found the USB device but cant communicate with any of the consoles. - -Try Running cr50_rma_open again. If it still fails unplug the ccd cable -for 5 seconds and plug it back in. -""" - -DEBUG_TOO_MANY_USB_DEVICES = """ -DEBUG SELECT USB: -More than one cr50 usb device was found. Disconnect all but one device -or use the -s option with the correct usb serialname. -""" - -DEBUG_ERASED_BOARD_ID = """ -DEBUG ERASED BOARD ID: -If you are using a prePVT device run -/usr/share/cros/cr50-set-board-id.sh proto - -If you are running a MP device, please talk to someone. -""" - -DEBUG_AUTHCODE_MISMATCH = """ -DEBUG AUTHCODE MISMATCH: - - Check the URL matches the one generated by the last cr50_rma_open - run. - - Check you used the correct authcode. - - Make sure the cr50 version is greater than 3.3. - - try generating another URL by rerunning the generate command and - rerunning the process. -""" - -DEBUG_DUT_CONTROL_OSERROR = """ -Run from chroot if you are trying to use a /dev/pts ccd servo console -""" - -class RMAOpen(object): - """Used to find the cr50 console and run RMA open""" - - ENABLE_TESTLAB_CMD = 'ccd testlab enabled\n' - - def __init__(self, device=None, usb_serial=None, servo_port=None, ip=None): - self.servo_port = servo_port if servo_port else '9999' - self.ip = ip - if device: - self.set_cr50_device(device) - elif servo_port: - self.find_cr50_servo_uart() - else: - self.find_cr50_device(usb_serial) - logging.info('DEVICE: %s', self.device) - self.check_version() - self.print_platform_info() - logging.info('Cr50 setup ok') - self.update_ccd_state() - self.using_ccd = self.device_is_running_with_servo_ccd() - - def _dut_control(self, control): - """Run dut-control and return the response""" - try: - cmd = ['dut-control', '-p', self.servo_port, control] - return subprocess.check_output(cmd, encoding='utf-8').strip() - except OSError: - logging.warning(DEBUG_DUT_CONTROL_OSERROR) - raise - - def find_cr50_servo_uart(self): - """Save the device used for the console. - - Find the console and configure it, so it can be used with this script. - """ - self._dut_control('cr50_uart_timestamp:off') - self.device = self._dut_control('cr50_uart_pty').split(':')[-1] - - def set_cr50_device(self, device): - """Save the device used for the console""" - self.device = device - - def send_cmd_get_output(self, cmd, nbytes=0): - """Send a cr50 command and get the output - - Args: - cmd: The cr50 command string - nbytes: The number of bytes to read from the console. If 0 read all - of the console output. - Returns: - The command output - """ - try: - ser = serial.Serial(self.device, timeout=1) - except OSError: - logging.warning('Permission denied %s', self.device) - logging.warning('Try running cr50_rma_open with sudo') - raise - write_cmd = cmd + '\n\n' - ser.write(write_cmd.encode('utf-8')) - if nbytes: - output = ser.read(nbytes) - else: - output = ser.readall() - ser.close() - - output = output.decode('utf-8').strip() if output else '' - # Return only the command output - split_cmd = cmd + '\r' - if cmd and split_cmd in output: - return ''.join(output.rpartition(split_cmd)[1::]).split('>')[0] - return output - - def device_is_running_with_servo_ccd(self): - """Return True if the device is a servod ccd console""" - # servod uses /dev/pts consoles. Non-servod uses /dev/ttyUSBX - if '/dev/pts' not in self.device: - return False - # If cr50 doesn't show rdd is connected, cr50 the device must not be - # a ccd device - if 'Rdd: connected' not in self.send_cmd_get_output('ccdstate'): - return False - # Check if the servod is running with ccd. This requires the script - # is run in the chroot, so run it last. - if 'ccd_cr50' not in self._dut_control('servo_type'): - return False - logging.info('running through servod ccd') - return True - - def get_rma_challenge(self): - """Get the rma_auth challenge - - There are two challenge formats - - " - ABEQ8 UGA4F AVEQP SHCKV - DGGPR N8JHG V8PNC LCHR2 - T27VF PRGBS N3ZXF RCCT2 - UBMKP ACM7E WUZUA A4GTN - " - and - " - generated challenge: - - CBYRYBEMH2Y75TC...rest of challenge - " - support extracting the challenge from both. - - Returns: - The RMA challenge with all whitespace removed. - """ - output = self.send_cmd_get_output('rma_auth').strip() - logging.info('rma_auth output:\n%s', output) - # Extract the challenge from the console output - if 'generated challenge:' in output: - return output.split('generated challenge:')[-1].strip() - challenge = ''.join(re.findall(r' \S{5}' * 4, output)) - # Remove all whitespace - return re.sub(r'\s', '', challenge) - - def generate_challenge_url(self, hwid): - """Get the rma_auth challenge - - Returns: - The RMA challenge with all whitespace removed. - """ - - challenge = self.get_rma_challenge() - self.print_platform_info() - logging.info('CHALLENGE: %s', challenge) - logging.info('HWID: %s', hwid) - url = URL % (challenge, hwid) - logging.info('GOTO:\n %s', url) - logging.info('If the server fails to debug the challenge make sure the ' - 'RLZ is allowlisted') - - def try_authcode(self, authcode): - """Try opening cr50 with the authcode - - Raises: - ValueError if there was no authcode match and ccd isn't open - """ - # rma_auth may cause the system to reboot. Don't wait to read all that - # output. Read the first 300 bytes and call it a day. - output = self.send_cmd_get_output('rma_auth ' + authcode, nbytes=300) - logging.info('CR50 RESPONSE: %s', output) - logging.info('waiting for cr50 reboot') - # Cr50 may be rebooting. Wait a bit - time.sleep(5) - if self.using_ccd: - # After reboot, reset the ccd endpoints - self._dut_control('power_state:ccd_reset') - # Update the ccd state after the authcode attempt - self.update_ccd_state() - - authcode_match = 'process_response: success!' in output - if not self.check(CCD_IS_UNRESTRICTED): - if not authcode_match: - logging.warning(DEBUG_AUTHCODE_MISMATCH) - message = 'Authcode mismatch. Check args and url' - else: - message = 'Could not set all capability privileges to Always' - raise ValueError(message) - - def wp_is_force_disabled(self): - """Returns True if write protect is forced disabled""" - output = self.send_cmd_get_output('wp') - wp_state = output.split('Flash WP:', 1)[-1].split('\n', 1)[0].strip() - logging.info('wp: %s', wp_state) - return wp_state == 'forced disabled' - - def testlab_is_enabled(self): - """Returns True if testlab mode is enabled""" - output = self.send_cmd_get_output('ccd testlab') - testlab_state = output.split('mode')[-1].strip().lower() - logging.info('testlab: %s', testlab_state) - return testlab_state == 'enabled' - - def ccd_is_restricted(self): - """Returns True if any of the capabilities are still restricted""" - output = self.send_cmd_get_output('ccd') - if 'Capabilities' not in output: - raise ValueError('Could not get ccd output') - logging.debug('CURRENT CCD SETTINGS:\n%s', output) - restricted = 'IfOpened' in output or 'IfUnlocked' in output - logging.info('ccd: %srestricted', '' if restricted else 'Un') - return restricted - - def update_ccd_state(self): - """Get the wp and ccd state from cr50. Save it in _ccd_state""" - self._ccd_state = 0 - if not self.ccd_is_restricted(): - self._ccd_state |= CCD_IS_UNRESTRICTED - if self.wp_is_force_disabled(): - self._ccd_state |= WP_IS_DISABLED - if self.testlab_is_enabled(): - self._ccd_state |= TESTLAB_IS_ENABLED - - def check(self, setting): - """Returns true if the all of the 1s in setting are 1 in _ccd_state""" - return self._ccd_state & setting == setting - - def _has_testlab_support(self): - """Return True if you can enable testlab mode""" - # all prepvt images can enable testlab - if self.is_prepvt: - return True - return not self._running_version_is_older(DEV_MODE_OPEN_PROD) - - def _capabilities_allow_open_from_console(self): - """Return True if ccd open is Always allowed from usb""" - output = self.send_cmd_get_output('ccd') - return (re.search('OpenNoDevMode.*Always', output) and - re.search('OpenFromUSB.*Always', output)) - - def _requires_dev_mode_open(self): - """Return True if the image requires dev mode to open""" - if self._capabilities_allow_open_from_console(): - return False - # All prod images that support 'open' require dev mode - if not self.is_prepvt: - return True - return not self._running_version_is_older(DEV_MODE_OPEN_PREPVT) - - def _run_on_dut(self, command): - """Run the command on the DUT.""" - return subprocess.check_output(['ssh', self.ip, command], - encoding='utf-8') - - def _open_in_dev_mode(self): - """Open Cr50 when it's in dev mode""" - output = self.send_cmd_get_output('ccd') - # If the device is already open, nothing needs to be done. - if 'State: Open' not in output: - # Verify the device is in devmode before trying to run open. - if 'dev_mode' not in output: - logging.warning('Enter dev mode to open ccd or update to %s', - TESTLAB_PROD) - raise ValueError('DUT not in dev mode') - if not self.ip: - logging.warning("If your DUT doesn't have ssh support, run " - "'gsctool -a -o' from the AP") - raise ValueError('Cannot run ccd open without dut ip') - self._run_on_dut('gsctool -a -o') - # Wait >1 second for cr50 to update ccd state - time.sleep(3) - output = self.send_cmd_get_output('ccd') - if 'State: Open' not in output: - raise ValueError('Could not open cr50') - logging.info('ccd is open') - - def enable_testlab(self): - """Disable write protect""" - if not self._has_testlab_support(): - logging.warning('Testlab mode is not supported in prod iamges') - return - # Some cr50 images need to be in dev mode before they can be opened. - if self._requires_dev_mode_open(): - self._open_in_dev_mode() - else: - self.send_cmd_get_output('ccd open') - logging.info('Enabling testlab mode reqires pressing the power button.') - logging.info('Once the process starts keep tapping the power button ' - 'for 10 seconds.') - input("Press Enter when you're ready to start...") - end_time = time.time() + 15 - - ser = serial.Serial(self.device, timeout=1) - printed_lines = '' - output = '' - # start ccd testlab enable - ser.write(self.ENABLE_TESTLAB_CMD.encode('utf-8')) - logging.info('start pressing the power button\n\n') - # Print all of the cr50 output as we get it, so the user will have more - # information about pressing the power button. Tapping the power button - # a couple of times should do it, but this will give us more confidence - # the process is still running/worked. - try: - while time.time() < end_time: - output += ser.read(100).decode('utf-8') - full_lines = output.rsplit('\n', 1)[0] - new_lines = full_lines - if printed_lines: - new_lines = full_lines.split(printed_lines, 1)[-1].strip() - logging.info('\n%s', new_lines) - printed_lines = full_lines - - # Make sure the process hasn't ended. If it has, print the last - # of the output and exit. - new_lines = output.split(printed_lines, 1)[-1] - if 'CCD test lab mode enabled' in output: - # print the last of the ou - logging.info(new_lines) - break - elif 'Physical presence check timeout' in output: - logging.info(new_lines) - logging.warning('Did not detect power button press in time') - raise ValueError('Could not enable testlab mode try again') - finally: - ser.close() - # Wait for the ccd hook to update things - time.sleep(3) - # Update the state after attempting to disable write protect - self.update_ccd_state() - if not self.check(TESTLAB_IS_ENABLED): - raise ValueError('Could not enable testlab mode try again') - - def wp_disable(self): - """Disable write protect""" - logging.info('Disabling write protect') - self.send_cmd_get_output('wp disable') - # Update the state after attempting to disable write protect - self.update_ccd_state() - if not self.check(WP_IS_DISABLED): - raise ValueError('Could not disable write protect') - - def check_version(self): - """Make sure cr50 is running a version that supports RMA Open""" - output = self.send_cmd_get_output('version') - if not output.strip(): - logging.warning(DEBUG_DEVICE, self.device) - raise ValueError('Could not communicate with %s' % self.device) - - version = re.search(r'RW.*\* ([\d\.]+)/', output).group(1) - logging.info('Running Cr50 Version: %s', version) - self.running_ver_fields = [int(field) for field in version.split('.')] - - # prePVT images have even major versions. Prod have odd - self.is_prepvt = self.running_ver_fields[1] % 2 == 0 - rma_support = RMA_SUPPORT_PREPVT if self.is_prepvt else RMA_SUPPORT_PROD - - logging.info('%s RMA support added in: %s', - 'prePVT' if self.is_prepvt else 'prod', rma_support) - if not self.is_prepvt and self._running_version_is_older(TESTLAB_PROD): - raise ValueError('Update cr50. No testlab support in old prod ' - 'images.') - if self._running_version_is_older(rma_support): - raise ValueError('%s does not have RMA support. Update to at ' - 'least %s' % (version, rma_support)) - - def _running_version_is_older(self, target_ver): - """Returns True if running version is older than target_ver.""" - target_ver_fields = [int(field) for field in target_ver.split('.')] - for i, field in enumerate(self.running_ver_fields): - if field > int(target_ver_fields[i]): - return False - return True - - def device_matches_devid(self, devid, device): - """Return True if the device matches devid. - - Use the sysinfo output from device to determine if it matches devid - - Returns: - True if sysinfo from device shows the given devid. False if there - is no output or sysinfo doesn't contain the devid. - """ - self.set_cr50_device(device) - sysinfo = self.send_cmd_get_output('sysinfo') - # Make sure there is some output, and it shows it's from Cr50 - if not sysinfo or 'cr50' not in sysinfo: - return False - logging.debug('Sysinfo output: %s', sysinfo) - # The cr50 device id should be in the sysinfo output, if we found - # the right console. Make sure it is - return devid in sysinfo - - def find_cr50_device(self, usb_serial): - """Find the cr50 console device - - The Cr50 usb serialname matches the cr50 devid. Convert the serialname - to devid. Use that to check all of the consoles and find cr50's. - - Args: - usb_serial: an optional string. The serialname of the cr50 usb - device - Raises: - ValueError if the console can't be found with the given serialname - """ - usb_serial = self.find_cr50_usb(usb_serial) - logging.info('SERIALNAME: %s', usb_serial) - devid = '0x' + ' 0x'.join(usb_serial.lower().split('-')) - logging.info('DEVID: %s', devid) - - # Get all the usb devices - devices = glob.glob('/dev/ttyUSB*') - # Typically Cr50 has the lowest number. Sort the devices, so we're more - # likely to try the cr50 console first. - devices.sort() - - # Find the one that is the cr50 console - for device in devices: - logging.info('testing %s', device) - if self.device_matches_devid(devid, device): - logging.info('found device: %s', device) - return - logging.warning(DEBUG_CONNECTION) - raise ValueError('Found USB device, but could not communicate with ' - 'cr50 console') - - def print_platform_info(self): - """Print the cr50 BID RLZ code""" - bid_output = self.send_cmd_get_output('bid') - bid = re.search(r'Board ID: (\S+?)[:,]', bid_output).group(1) - if bid == ERASED_BID: - logging.warning(DEBUG_ERASED_BOARD_ID) - raise ValueError('Cannot run RMA Open when board id is erased') - bid = int(bid, 16) - chrs = [chr((bid >> (8 * i)) & 0xff) for i in range(4)] - logging.info('RLZ: %s', ''.join(chrs[::-1])) - - @staticmethod - def find_cr50_usb(usb_serial): - """Make sure the Cr50 USB device exists""" - try: - output = subprocess.check_output(CR50_LSUSB_CMD, encoding='utf-8') - except: - logging.warning(DEBUG_MISSING_USB) - raise ValueError('Could not find Cr50 USB device') - serialnames = re.findall(r'iSerial +\d+ (\S+)\s', output) - if usb_serial: - if usb_serial not in serialnames: - logging.warning(DEBUG_SERIALNAME) - raise ValueError('Could not find usb device "%s"' % usb_serial) - return usb_serial - if len(serialnames) > 1: - logging.info('Found Cr50 device serialnames %s', - ', '.join(serialnames)) - logging.warning(DEBUG_TOO_MANY_USB_DEVICES) - raise ValueError('Too many cr50 usb devices') - return serialnames[0] - - def print_dut_state(self): - """Print CCD RMA and testlab mode state.""" - if not self.check(CCD_IS_UNRESTRICTED): - logging.info('CCD is still restricted.') - logging.info('Run cr50_rma_open.py -g -i $HWID to generate a url') - logging.info('Run cr50_rma_open.py -a $AUTHCODE to open cr50 with ' - 'an authcode') - elif not self.check(WP_IS_DISABLED): - logging.info('WP is still enabled.') - logging.info('Run cr50_rma_open.py -w to disable write protect') - if self.check(RMA_OPENED): - logging.info('RMA Open complete') - - if not self.check(TESTLAB_IS_ENABLED) and self.is_prepvt: - logging.info('testlab mode is disabled.') - logging.info('If you are prepping a device for the testlab, you ' - 'should enable testlab mode.') - logging.info('Run cr50_rma_open.py -t to enable testlab mode') - - -def parse_args(argv): - """Get cr50_rma_open args.""" - parser = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('-g', '--generate_challenge', action='store_true', - help='Generate Cr50 challenge. Must be used with -i') - parser.add_argument('-t', '--enable_testlab', action='store_true', - help='enable testlab mode') - parser.add_argument('-w', '--wp_disable', action='store_true', - help='Disable write protect') - parser.add_argument('-c', '--check_connection', action='store_true', - help='Check cr50 console connection works') - parser.add_argument('-s', '--serialname', type=str, default='', - help='The cr50 usb serialname') - parser.add_argument('-D', '--debug', action='store_true', - help='print debug messages') - parser.add_argument('-d', '--device', type=str, default='', - help='cr50 console device ex /dev/ttyUSB0') - parser.add_argument('-i', '--hwid', type=str, default='', - help='The board hwid. Needed to generate a challenge') - parser.add_argument('-a', '--authcode', type=str, default='', - help='The authcode string from the challenge url') - parser.add_argument('-P', '--servo_port', type=str, default='', - help='the servo port') - parser.add_argument('-I', '--ip', type=str, default='', - help='The DUT IP. Necessary to do ccd open') - return parser.parse_args(argv) - - -def main(argv): - """Run cr50 rma open.""" - opts = parse_args(argv) - - loglevel = logging.INFO - log_format = '%(levelname)7s' - if opts.debug: - loglevel = logging.DEBUG - log_format += ' - %(lineno)3d:%(funcName)-15s' - log_format += ' - %(message)s' - logging.basicConfig(level=loglevel, format=log_format) - - tried_authcode = False - logging.info('Running cr50_rma_open version %s', SCRIPT_VERSION) - - cr50_rma_open = RMAOpen(opts.device, opts.serialname, opts.servo_port, - opts.ip) - if opts.check_connection: - sys.exit(0) - - if not cr50_rma_open.check(CCD_IS_UNRESTRICTED): - if opts.generate_challenge: - if not opts.hwid: - logging.warning('--hwid necessary to generate challenge url') - sys.exit(0) - cr50_rma_open.generate_challenge_url(opts.hwid) - sys.exit(0) - elif opts.authcode: - logging.info('Using authcode: %s', opts.authcode) - cr50_rma_open.try_authcode(opts.authcode) - tried_authcode = True - - if not cr50_rma_open.check(WP_IS_DISABLED) and (tried_authcode or - opts.wp_disable): - if not cr50_rma_open.check(CCD_IS_UNRESTRICTED): - raise ValueError("Can't disable write protect unless ccd is " - "open. Run through the rma open process first") - if tried_authcode: - logging.warning('RMA Open did not disable write protect. File a ' - 'bug') - logging.warning('Trying to disable it manually') - cr50_rma_open.wp_disable() - - if not cr50_rma_open.check(TESTLAB_IS_ENABLED) and opts.enable_testlab: - if not cr50_rma_open.check(CCD_IS_UNRESTRICTED): - raise ValueError("Can't enable testlab mode unless ccd is open." - "Run through the rma open process first") - cr50_rma_open.enable_testlab() - - cr50_rma_open.print_dut_state() - - -if __name__ == '__main__': - sys.exit(main(sys.argv[1:])) |