1# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""A utility to program Chrome OS devices' firmware using servo.
6
7This utility expects the DUT to be connected to a servo device. This allows us
8to put the DUT into the required state and to actually program the DUT's
9firmware using FTDI, USB and/or serial interfaces provided by servo.
10
11Servo state is preserved across the programming process.
12"""
13
14import glob
15import logging
16import os
17import re
18import site
19import time
20import xml.etree.ElementTree
21
22from autotest_lib.client.common_lib import error
23from autotest_lib.server.cros.faft.config.config import Config as FAFTConfig
24
25
26# Number of seconds for program EC/BIOS to time out.
27FIRMWARE_PROGRAM_TIMEOUT_SEC = 600
28
29class ProgrammerError(Exception):
30    """Local exception class wrapper."""
31    pass
32
33
34class _BaseProgrammer(object):
35    """Class implementing base programmer services.
36
37    Private attributes:
38      _servo: a servo object controlling the servo device
39      _servo_prog_state: a tuple of strings of "<control>:<value>" pairs,
40                         listing servo controls and their required values for
41                         programming
42      _servo_prog_state_delay: time in second to wait after changing servo
43                               controls for programming.
44      _servo_saved_state: a list of the same elements as _servo_prog_state,
45                          those which need to be restored after programming
46      _program_cmd: a string, the shell command to run on the servo host
47                    to actually program the firmware. Dependent on
48                    firmware/hardware type, set by subclasses.
49    """
50
51    def __init__(self, servo, req_list):
52        """Base constructor.
53        @param servo: a servo object controlling the servo device
54        @param req_list: a list of strings, names of the utilities required
55                         to be in the path for the programmer to succeed
56        """
57        self._servo = servo
58        self._servo_prog_state = ()
59        self._servo_prog_state_delay = 0
60        self._servo_saved_state = []
61        self._program_cmd = ''
62        try:
63            self._servo.system('which %s' % ' '.join(req_list))
64        except error.AutoservRunError:
65            # TODO: We turn this exception into a warn since the fw programmer
66            # is not working right now, and some systems do not package the
67            # required utilities its checking for.
68            # We should reinstate this exception once the programmer is working
69            # to indicate the missing utilities earlier in the test cycle.
70            # Bug chromium:371011 filed to track this.
71            logging.warn("Ignoring exception when verify required bins : %s",
72                         ' '.join(req_list))
73
74
75    def _set_servo_state(self):
76        """Set servo for programming, while saving the current state."""
77        logging.debug("Setting servo state for programming")
78        for item in self._servo_prog_state:
79            key, value = item.split(':')
80            try:
81                present = self._servo.get(key)
82            except error.TestFail:
83                logging.warn('Missing servo control: %s', key)
84                continue
85            if present != value:
86                self._servo_saved_state.append('%s:%s' % (key, present))
87            self._servo.set(key, value)
88        time.sleep(self._servo_prog_state_delay)
89
90
91    def _restore_servo_state(self):
92        """Restore previously saved servo state."""
93        logging.debug("Restoring servo state after programming")
94        self._servo_saved_state.reverse()  # Do it in the reverse order.
95        for item in self._servo_saved_state:
96            key, value = item.split(':')
97            self._servo.set(key, value)
98
99
100    def program(self):
101        """Program the firmware as configured by a subclass."""
102        self._set_servo_state()
103        try:
104            logging.debug("Programmer command: %s", self._program_cmd)
105            self._servo.system(self._program_cmd,
106                               timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC)
107        finally:
108            self._restore_servo_state()
109
110
111class FlashromProgrammer(_BaseProgrammer):
112    """Class for programming AP flashrom."""
113
114    def __init__(self, servo, keep_ro=False):
115        """Configure required servo state.
116
117        @param servo: a servo object controlling the servo device
118        @param keep_ro: True to keep the RO portion unchanged
119        """
120        super(FlashromProgrammer, self).__init__(servo, ['flashrom',])
121        self._keep_ro = keep_ro
122        self._fw_path = None
123        self._tmp_path = '/tmp'
124        self._fw_main = os.path.join(self._tmp_path, 'fw_main')
125        self._wp_ro = os.path.join(self._tmp_path, 'wp_ro')
126        self._ro_vpd = os.path.join(self._tmp_path, 'ro_vpd')
127        self._rw_vpd = os.path.join(self._tmp_path, 'rw_vpd')
128        self._gbb = os.path.join(self._tmp_path, 'gbb')
129        self._servo_version = self._servo.get_servo_version()
130        self._servo_serials = self._servo._server.get_servo_serials()
131
132
133    def program(self):
134        """Program the firmware but preserve VPD and HWID."""
135        assert self._fw_path is not None
136        self._set_servo_state()
137        try:
138            wp_ro_section = [('WP_RO', self._wp_ro)]
139            rw_vpd_section = [('RW_VPD', self._rw_vpd)]
140            ro_vpd_section = [('RO_VPD', self._ro_vpd)]
141            gbb_section = [('GBB', self._gbb)]
142            if self._keep_ro:
143                # Keep the whole RO portion
144                preserved_sections = wp_ro_section + rw_vpd_section
145            else:
146                preserved_sections = ro_vpd_section + rw_vpd_section
147
148            servo_v2_programmer = 'ft2232_spi:type=servo-v2'
149            servo_v3_programmer = 'linux_spi'
150            servo_v4_with_micro_programmer = 'raiden_spi'
151            servo_v4_with_ccd_programmer = 'raiden_debug_spi:target=AP'
152            if self._servo_version == 'servo_v2':
153                programmer = servo_v2_programmer
154                servo_serial = self._servo_serials.get('main')
155                if servo_serial:
156                    programmer += ',serial=%s' % servo_serial
157            elif self._servo_version == 'servo_v3':
158                programmer = servo_v3_programmer
159            elif self._servo_version == 'servo_v4':
160                # Get the serial of the servo micro if it exists.
161                servo_micro_serial = self._servo_serials.get('servo_micro')
162                ccd_serial = self._servo_serials.get('ccd')
163                # When a uServo is connected to a DUT with CCD support, the
164                # firmware programmer will always use the uServo to program.
165                if servo_micro_serial:
166                    programmer = servo_v4_with_micro_programmer
167                    programmer += ',serial=%s' % servo_micro_serial
168                elif ccd_serial:
169                    programmer = servo_v4_with_ccd_programmer
170                    programmer += ',serial=%s' % ccd_serial
171            else:
172                raise Exception('Servo version %s is not supported.' %
173                                self._servo_version)
174            # Save needed sections from current firmware
175            for section in preserved_sections + gbb_section:
176                self._servo.system(' '.join([
177                    'flashrom', '-V', '-p', programmer,
178                    '-r', self._fw_main, '-i', '%s:%s' % section]),
179                    timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC)
180
181            # Pack the saved VPD into new firmware
182            self._servo.system('cp %s %s' % (self._fw_path, self._fw_main))
183            img_size = self._servo.system_output(
184                    "stat -c '%%s' %s" % self._fw_main)
185            pack_cmd = ['flashrom',
186                    '-p', 'dummy:image=%s,size=%s,emulate=VARIABLE_SIZE' % (
187                        self._fw_main, img_size),
188                    '-w', self._fw_main]
189            for section in preserved_sections:
190                pack_cmd.extend(['-i', '%s:%s' % section])
191            self._servo.system(' '.join(pack_cmd),
192                               timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC)
193
194            # HWID is inside the RO portion. Don't preserve HWID if we keep RO.
195            if not self._keep_ro:
196                # Read original HWID. The output format is:
197                #    hardware_id: RAMBI TEST A_A 0128
198                gbb_hwid_output = self._servo.system_output(
199                        'gbb_utility -g --hwid %s' % self._gbb)
200                original_hwid = gbb_hwid_output.split(':', 1)[1].strip()
201
202                # Write HWID to new firmware
203                self._servo.system("gbb_utility -s --hwid='%s' %s" %
204                        (original_hwid, self._fw_main))
205
206            # Flash the new firmware
207            self._servo.system(' '.join([
208                    'flashrom', '-V', '-p', programmer,
209                    '-w', self._fw_main]), timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC)
210        finally:
211            self._servo.get_power_state_controller().reset()
212            self._restore_servo_state()
213
214
215    def prepare_programmer(self, path):
216        """Prepare programmer for programming.
217
218        @param path: a string, name of the file containing the firmware image.
219        """
220        self._fw_path = path
221        # CCD takes care holding AP/EC. Don't need the following steps.
222        if not (self._servo_version == 'servo_v4' and
223                'ccd' in self._servo_serials):
224            faft_config = FAFTConfig(self._servo.get_board())
225            self._servo_prog_state_delay = faft_config.servo_prog_state_delay
226            self._servo_prog_state = (
227                'spi2_vref:%s' % faft_config.spi_voltage,
228                'spi2_buf_en:on',
229                'spi2_buf_on_flex_en:on',
230                'spi_hold:off',
231                'cold_reset:on',
232                'usbpd_reset:on',
233                )
234
235
236class FlashECProgrammer(_BaseProgrammer):
237    """Class for programming AP flashrom."""
238
239    def __init__(self, servo):
240        """Configure required servo state."""
241        super(FlashECProgrammer, self).__init__(servo, ['flash_ec',])
242        self._servo = servo
243        self._servo_serials = self._servo._server.get_servo_serials()
244
245    def prepare_programmer(self, image):
246        """Prepare programmer for programming.
247
248        @param image: string with the location of the image file
249        """
250        port = self._servo._servo_host.servo_port
251        self._program_cmd = ('flash_ec --chip=%s --image=%s --port=%d' %
252                             (self._servo.get('ec_chip'), image, port))
253        if 'ccd' in self._servo_serials:
254            self._program_cmd += ' --raiden'
255
256
257class ProgrammerV2(object):
258    """Main programmer class which provides programmer for BIOS and EC with
259    servo V2."""
260
261    def __init__(self, servo):
262        self._servo = servo
263        self._valid_boards = self._get_valid_v2_boards()
264        self._bios_programmer = self._factory_bios(self._servo)
265        self._ec_programmer = self._factory_ec(self._servo)
266
267
268    @staticmethod
269    def _get_valid_v2_boards():
270        """Greps servod config files to look for valid v2 boards.
271
272        @return A list of valid board names.
273        """
274        site_packages_paths = site.getsitepackages()
275        SERVOD_CONFIG_DATA_DIR = None
276        for p in site_packages_paths:
277            servo_data_path = os.path.join(p, 'servo', 'data')
278            if os.path.exists(servo_data_path):
279                SERVOD_CONFIG_DATA_DIR = servo_data_path
280                break
281        if not SERVOD_CONFIG_DATA_DIR:
282            raise ProgrammerError(
283                    'Unable to locate data directory of Python servo module')
284        SERVOFLEX_V2_R0_P50_CONFIG = 'servoflex_v2_r0_p50.xml'
285        SERVO_CONFIG_GLOB = 'servo_*_overlay.xml'
286        SERVO_CONFIG_REGEXP = 'servo_(?P<board>.+)_overlay.xml'
287
288        def is_v2_compatible_board(board_config_path):
289            """Check if the given board config file is v2-compatible.
290
291            @param board_config_path: Path to a board config XML file.
292
293            @return True if the board is v2-compatible; False otherwise.
294            """
295            configs = []
296            def get_all_includes(config_path):
297                """Get all included XML config names in the given config file.
298
299                @param config_path: Path to a servo config file.
300                """
301                root = xml.etree.ElementTree.parse(config_path).getroot()
302                for element in root.findall('include'):
303                    include_name = element.find('name').text
304                    configs.append(include_name)
305                    get_all_includes(os.path.join(
306                            SERVOD_CONFIG_DATA_DIR, include_name))
307
308            get_all_includes(board_config_path)
309            return True if SERVOFLEX_V2_R0_P50_CONFIG in configs else False
310
311        result = []
312        board_overlays = glob.glob(
313                os.path.join(SERVOD_CONFIG_DATA_DIR, SERVO_CONFIG_GLOB))
314        for overlay_path in board_overlays:
315            if is_v2_compatible_board(overlay_path):
316                result.append(re.search(SERVO_CONFIG_REGEXP,
317                                        overlay_path).group('board'))
318        return result
319
320
321    def _get_flashrom_programmer(self, servo):
322        """Gets a proper flashrom programmer.
323
324        @param servo: A servo object.
325
326        @return A programmer for flashrom.
327        """
328        return FlashromProgrammer(servo)
329
330
331    def _factory_bios(self, servo):
332        """Instantiates and returns (bios, ec) programmers for the board.
333
334        @param servo: A servo object.
335
336        @return A programmer for ec. If the programmer is not supported
337            for the board, None will be returned.
338        """
339        _bios_prog = None
340        _board = servo.get_board()
341
342        logging.debug('Setting up BIOS programmer for board: %s', _board)
343        if _board in self._valid_boards:
344            _bios_prog = self._get_flashrom_programmer(servo)
345        else:
346            logging.warning('No BIOS programmer found for board: %s', _board)
347
348        return _bios_prog
349
350
351    def _factory_ec(self, servo):
352        """Instantiates and returns ec programmer for the board.
353
354        @param servo: A servo object.
355
356        @return A programmer for ec. If the programmer is not supported
357            for the board, None will be returned.
358        """
359        _ec_prog = None
360        _board = servo.get_board()
361
362        logging.debug('Setting up EC programmer for board: %s', _board)
363        if _board in self._valid_boards:
364            _ec_prog = FlashECProgrammer(servo)
365        else:
366            logging.warning('No EC programmer found for board: %s', _board)
367
368        return _ec_prog
369
370
371    def program_bios(self, image):
372        """Programs the DUT with provide bios image.
373
374        @param image: (required) location of bios image file.
375
376        """
377        self._bios_programmer.prepare_programmer(image)
378        self._bios_programmer.program()
379
380
381    def program_ec(self, image):
382        """Programs the DUT with provide ec image.
383
384        @param image: (required) location of ec image file.
385
386        """
387        self._ec_programmer.prepare_programmer(image)
388        self._ec_programmer.program()
389
390
391class ProgrammerV2RwOnly(ProgrammerV2):
392    """Main programmer class which provides programmer for only updating the RW
393    portion of BIOS with servo V2.
394
395    It does nothing on EC, as EC software sync on the next boot will
396    automatically overwrite the EC RW portion, using the EC RW image inside
397    the BIOS RW image.
398
399    """
400
401    def _get_flashrom_programmer(self, servo):
402        """Gets a proper flashrom programmer.
403
404        @param servo: A servo object.
405
406        @return A programmer for flashrom.
407        """
408        return FlashromProgrammer(servo, keep_ro=True)
409
410
411    def program_ec(self, image):
412        """Programs the DUT with provide ec image.
413
414        @param image: (required) location of ec image file.
415
416        """
417        # Do nothing. EC software sync will update the EC RW.
418        pass
419
420
421class ProgrammerV3(object):
422    """Main programmer class which provides programmer for BIOS and EC with
423    servo V3.
424
425    Different from programmer for servo v2, programmer for servo v3 does not
426    try to validate if the board can use servo V3 to update firmware. As long as
427    the servod process running in beagblebone with given board, the program will
428    attempt to flash bios and ec.
429
430    """
431
432    def __init__(self, servo):
433        self._servo = servo
434        self._bios_programmer = FlashromProgrammer(servo)
435        self._ec_programmer = FlashECProgrammer(servo)
436
437
438    def program_bios(self, image):
439        """Programs the DUT with provide bios image.
440
441        @param image: (required) location of bios image file.
442
443        """
444        self._bios_programmer.prepare_programmer(image)
445        self._bios_programmer.program()
446
447
448    def program_ec(self, image):
449        """Programs the DUT with provide ec image.
450
451        @param image: (required) location of ec image file.
452
453        """
454        self._ec_programmer.prepare_programmer(image)
455        self._ec_programmer.program()
456
457
458class ProgrammerV3RwOnly(ProgrammerV3):
459    """Main programmer class which provides programmer for only updating the RW
460    portion of BIOS with servo V3.
461
462    It does nothing on EC, as EC software sync on the next boot will
463    automatically overwrite the EC RW portion, using the EC RW image inside
464    the BIOS RW image.
465
466    """
467
468    def __init__(self, servo):
469        self._servo = servo
470        self._bios_programmer = FlashromProgrammer(servo, keep_ro=True)
471
472
473    def program_ec(self, image):
474        """Programs the DUT with provide ec image.
475
476        @param image: (required) location of ec image file.
477
478        """
479        # Do nothing. EC software sync will update the EC RW.
480        pass
481