• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python2
2# Copyright 2016 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7Automatically update the afe_stable_versions table.
8
9This command updates the stable repair version for selected boards
10in the lab.  For each board, if the version that Omaha is serving
11on the Beta channel for the board is more recent than the current
12stable version in the AFE database, then the AFE is updated to use
13the version on Omaha.
14
15The upgrade process is applied to every "managed board" in the test
16lab.  Generally, a managed board is a board with both spare and
17critical scheduling pools.
18
19See `autotest_lib.site_utils.lab_inventory` for the full definition
20of "managed board".
21
22The command supports a `--dry-run` option that reports changes that
23would be made, without making the actual RPC calls to change the
24database.
25
26"""
27
28import argparse
29import logging
30
31import common
32from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
33from autotest_lib.site_utils import lab_inventory
34from autotest_lib.site_utils import loglib
35from autotest_lib.site_utils.stable_images import build_data
36from chromite.lib import ts_mon_config
37from chromite.lib import metrics
38
39
40# _DEFAULT_BOARD - The distinguished board name used to identify a
41# stable version mapping that is used for any board without an explicit
42# mapping of its own.
43#
44# _DEFAULT_VERSION_TAG - A string used to signify that there is no
45# mapping for a board, in other words, the board is mapped to the
46# default version.
47#
48_DEFAULT_BOARD = 'DEFAULT'
49_DEFAULT_VERSION_TAG = '(default)'
50
51_METRICS_PREFIX = 'chromeos/autotest/assign_stable_images'
52
53
54class _VersionUpdater(object):
55    """
56    Class to report and apply version changes.
57
58    This class is responsible for the low-level logic of applying
59    version upgrades and reporting them as command output.
60
61    This class exists to solve two problems:
62     1. To distinguish "normal" vs. "dry-run" modes.  Each mode has a
63        subclass; methods that perform actual AFE updates are
64        implemented for the normal mode subclass only.
65     2. To provide hooks for unit tests.  The unit tests override both
66        the reporting and modification behaviors, in order to test the
67        higher level logic that decides what changes are needed.
68
69    Methods meant merely to report changes to command output have names
70    starting with "report" or "_report".  Methods that are meant to
71    change the AFE in normal mode have names starting with "_do"
72    """
73
74    def __init__(self, afe, dry_run):
75        """Initialize us.
76
77        @param afe:     A frontend.AFE object.
78        @param dry_run: A boolean indicating whether to execute in dry run mode.
79                        No updates are persisted to the afe in dry run.
80        """
81        self._dry_run = dry_run
82        image_types = [afe.CROS_IMAGE_TYPE, afe.FIRMWARE_IMAGE_TYPE]
83        self._version_maps = {
84            image_type: afe.get_stable_version_map(image_type)
85                for image_type in image_types
86        }
87        self._cros_map = self._version_maps[afe.CROS_IMAGE_TYPE]
88        self._selected_map = None
89
90    def select_version_map(self, image_type):
91        """
92        Select an AFE version map object based on `image_type`.
93
94        This creates and remembers an AFE version mapper object to be
95        used for making changes in normal mode.
96
97        @param image_type   Image type parameter for the version mapper
98                            object.
99        """
100        self._selected_map = self._version_maps[image_type]
101        return self._selected_map.get_all_versions()
102
103    def report_default_changed(self, old_default, new_default):
104        """
105        Report that the default version mapping is changing.
106
107        This merely reports a text description of the pending change
108        without executing it.
109
110        @param old_default  The original default version.
111        @param new_default  The new default version to be applied.
112        """
113        logging.debug('Default %s -> %s', old_default, new_default)
114
115    def _report_board_changed(self, board, old_version, new_version):
116        """
117        Report a change in one board's assigned version mapping.
118
119        This merely reports a text description of the pending change
120        without executing it.
121
122        @param board        The board with the changing version.
123        @param old_version  The original version mapped to the board.
124        @param new_version  The new version to be applied to the board.
125        """
126        logging.debug('    %-22s %s -> %s', board, old_version, new_version)
127
128    def report_board_unchanged(self, board, old_version):
129        """
130        Report that a board's version mapping is unchanged.
131
132        This reports that a board has a non-default mapping that will be
133        unchanged.
134
135        @param board        The board that is not changing.
136        @param old_version  The board's version mapping.
137        """
138        self._report_board_changed(board, '(no change)', old_version)
139
140    def _do_set_mapping(self, board, new_version):
141        """
142        Change one board's assigned version mapping.
143
144        @param board        The board with the changing version.
145        @param new_version  The new version to be applied to the board.
146        """
147        if self._dry_run:
148            logging.info('DRYRUN: Would have set %s version to %s',
149                         board, new_version)
150        else:
151            self._selected_map.set_version(board, new_version)
152
153    def _do_delete_mapping(self, board):
154        """
155        Delete one board's assigned version mapping.
156
157        @param board        The board with the version to be deleted.
158        """
159        if self._dry_run:
160            logging.info('DRYRUN: Would have deleted version for %s', board)
161        else:
162            self._selected_map.delete_version(board)
163
164    def set_mapping(self, board, old_version, new_version):
165        """
166        Change and report a board version mapping.
167
168        @param board        The board with the changing version.
169        @param old_version  The original version mapped to the board.
170        @param new_version  The new version to be applied to the board.
171        """
172        self._report_board_changed(board, old_version, new_version)
173        self._do_set_mapping(board, new_version)
174
175    def upgrade_default(self, new_default):
176        """
177        Apply a default version change.
178
179        @param new_default  The new default version to be applied.
180        """
181        self._do_set_mapping(_DEFAULT_BOARD, new_default)
182
183    def delete_mapping(self, board, old_version):
184        """
185        Delete a board version mapping, and report the change.
186
187        @param board        The board with the version to be deleted.
188        @param old_version  The board's verson prior to deletion.
189        """
190        assert board != _DEFAULT_BOARD
191        self._report_board_changed(board,
192                                   old_version,
193                                   _DEFAULT_VERSION_TAG)
194        self._do_delete_mapping(board)
195
196
197def _get_upgrade_versions(cros_versions, omaha_versions, boards):
198    """
199    Get the new stable versions to which we should update.
200
201    The new versions are returned as a tuple of a dictionary mapping
202    board names to versions, plus a new default board setting.  The
203    new default is determined as the most commonly used version
204    across the given boards.
205
206    The new dictionary will have a mapping for every board in `boards`.
207    That mapping will be taken from `cros_versions`, unless the board has
208    a mapping in `omaha_versions` _and_ the omaha version is more recent
209    than the AFE version.
210
211    @param cros_versions    The current board->version mappings in the
212                            AFE.
213    @param omaha_versions   The current board->version mappings from
214                            Omaha for the Beta channel.
215    @param boards           Set of boards to be upgraded.
216    @return Tuple of (mapping, default) where mapping is a dictionary
217            mapping boards to versions, and default is a version string.
218    """
219    upgrade_versions = {}
220    version_counts = {}
221    afe_default = cros_versions[_DEFAULT_BOARD]
222    for board in boards:
223        version = build_data.get_omaha_upgrade(
224                omaha_versions, board,
225                cros_versions.get(board, afe_default))
226        upgrade_versions[board] = version
227        version_counts.setdefault(version, 0)
228        version_counts[version] += 1
229    return (upgrade_versions,
230            max(version_counts.items(), key=lambda x: x[1])[0])
231
232
233def _get_firmware_upgrades(cros_versions):
234    """
235    Get the new firmware versions to which we should update.
236
237    @param cros_versions    Current board->cros version mappings in the
238                            AFE.
239    @return A dictionary mapping boards/models to firmware upgrade versions.
240            If the build is unibuild, the key is a model name; else, the key
241            is a board name.
242    """
243    firmware_upgrades = {}
244    for board, version in cros_versions.iteritems():
245        firmware_upgrades.update(
246            build_data.get_firmware_versions(board, version))
247    return firmware_upgrades
248
249
250def _apply_cros_upgrades(updater, old_versions, new_versions,
251                         new_default):
252    """
253    Change CrOS stable version mappings in the AFE.
254
255    The input `old_versions` dictionary represents the content of the
256    `afe_stable_versions` database table; it contains mappings for a
257    default version, plus exceptions for boards with non-default
258    mappings.
259
260    The `new_versions` dictionary contains a mapping for every board,
261    including boards that will be mapped to the new default version.
262
263    This function applies the AFE changes necessary to produce the new
264    AFE mappings indicated by `new_versions` and `new_default`.  The
265    changes are ordered so that at any moment, every board is mapped
266    either according to the old or the new mapping.
267
268    @param updater        Instance of _VersionUpdater responsible for
269                          making the actual database changes.
270    @param old_versions   The current board->version mappings in the
271                          AFE.
272    @param new_versions   New board->version mappings obtained by
273                          applying Beta channel upgrades from Omaha.
274    @param new_default    The new default build for the AFE.
275    """
276    old_default = old_versions[_DEFAULT_BOARD]
277    if old_default != new_default:
278        updater.report_default_changed(old_default, new_default)
279    logging.info('Applying stable version changes:')
280    default_count = 0
281    for board, new_build in new_versions.items():
282        if new_build == new_default:
283            default_count += 1
284        elif board in old_versions and new_build == old_versions[board]:
285            updater.report_board_unchanged(board, new_build)
286        else:
287            old_build = old_versions.get(board)
288            if old_build is None:
289                old_build = _DEFAULT_VERSION_TAG
290            updater.set_mapping(board, old_build, new_build)
291    if old_default != new_default:
292        updater.upgrade_default(new_default)
293    for board, new_build in new_versions.items():
294        if new_build == new_default and board in old_versions:
295            updater.delete_mapping(board, old_versions[board])
296    logging.info('%d boards now use the default mapping', default_count)
297
298
299def _apply_firmware_upgrades(updater, old_versions, new_versions):
300    """
301    Change firmware version mappings in the AFE.
302
303    The input `old_versions` dictionary represents the content of the
304    firmware mappings in the `afe_stable_versions` database table.
305    There is no default version; missing boards simply have no current
306    version.
307
308    This function applies the AFE changes necessary to produce the new
309    AFE mappings indicated by `new_versions`.
310
311    TODO(jrbarnette) This function ought to remove any mapping not found
312    in `new_versions`.  However, in theory, that's only needed to
313    account for boards that are removed from the lab, and that hasn't
314    happened yet.
315
316    @param updater        Instance of _VersionUpdater responsible for
317                          making the actual database changes.
318    @param old_versions   The current board->version mappings in the
319                          AFE.
320    @param new_versions   New board->version mappings obtained by
321                          applying Beta channel upgrades from Omaha.
322    """
323    unchanged = 0
324    no_version = 0
325    for board, new_firmware in new_versions.items():
326        if new_firmware is None:
327            no_version += 1
328        elif board not in old_versions:
329            updater.set_mapping(board, '(nothing)', new_firmware)
330        else:
331            old_firmware = old_versions[board]
332            if new_firmware != old_firmware:
333                updater.set_mapping(board, old_firmware, new_firmware)
334            else:
335                unchanged += 1
336    logging.info('%d boards have no firmware mapping', no_version)
337    logging.info('%d boards are unchanged', unchanged)
338
339
340def _assign_stable_images(arguments):
341    afe = frontend_wrappers.RetryingAFE(server=arguments.web)
342    updater = _VersionUpdater(afe, dry_run=arguments.dry_run)
343
344    cros_versions = updater.select_version_map(afe.CROS_IMAGE_TYPE)
345    omaha_versions = build_data.get_omaha_version_map()
346    upgrade_versions, new_default = (
347            _get_upgrade_versions(cros_versions, omaha_versions,
348                                  lab_inventory.get_managed_boards(afe)))
349    _apply_cros_upgrades(updater, cros_versions,
350                         upgrade_versions, new_default)
351
352    logging.info('Applying firmware updates.')
353    fw_versions = updater.select_version_map(afe.FIRMWARE_IMAGE_TYPE)
354    firmware_upgrades = _get_firmware_upgrades(upgrade_versions)
355    _apply_firmware_upgrades(updater, fw_versions, firmware_upgrades)
356
357
358def main():
359    """Standard main routine."""
360    parser = argparse.ArgumentParser(
361            description='Update the stable repair version for all '
362                        'boards')
363    parser.add_argument('-n', '--dry-run',
364                        action='store_true',
365                        help='print changes without executing them')
366    loglib.add_logging_options(parser)
367    # TODO(crbug/888046) Make these arguments required once puppet is updated to
368    # pass them in.
369    parser.add_argument('--web',
370                        default='cautotest',
371                        help='URL to the AFE to update.')
372
373    arguments = parser.parse_args()
374    loglib.configure_logging_with_args(parser, arguments)
375
376    tsmon_args = {
377            'service_name': parser.prog,
378            'indirect': False,
379            'auto_flush': False,
380    }
381    if arguments.dry_run:
382        logging.info('DRYRUN: No changes will be made.')
383        # metrics will be logged to logging stream anyway.
384        tsmon_args['debug_file'] = '/dev/null'
385
386    try:
387        with ts_mon_config.SetupTsMonGlobalState(**tsmon_args):
388            with metrics.SuccessCounter(_METRICS_PREFIX + '/tick',
389                                        fields={'afe': arguments.web}):
390                _assign_stable_images(arguments)
391    finally:
392        metrics.Flush()
393
394if __name__ == '__main__':
395    main()
396