Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | # |
| 3 | # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | """Tool for downloading and processing the latest Buildbot builds. |
| 8 | |
| 9 | Downloader is a tool for downloading and processing images for the various board |
| 10 | types supported by ChromeOS. |
| 11 | |
| 12 | All downloading and processing is driven by a board to archive server mapping in |
| 13 | a specified JSON config file. Boards are processed sequentially. |
| 14 | |
| 15 | Downloader is multi-instance friendly. You can spin up as many instances as |
| 16 | necessary to handle image processing load (which can be substantial). It is not |
| 17 | recommended to run more than one instance per machine. |
| 18 | |
| 19 | Downloader expects the JSON config file to be in the current working directory |
| 20 | or to be run with --config pointing to the actual config file. |
| 21 | """ |
| 22 | |
| 23 | __author__ = 'dalecurtis@google.com (Dale Curtis)' |
| 24 | |
| 25 | import logging |
| 26 | import optparse |
| 27 | import os |
| 28 | import re |
| 29 | import shutil |
| 30 | |
| 31 | from chromeos_test import autotest_util |
| 32 | from chromeos_test import build_util |
| 33 | from chromeos_test import common_util |
| 34 | from chromeos_test import dash_util |
| 35 | from chromeos_test import dev_server |
| 36 | from chromeos_test import log_util |
| 37 | from chromeos_test import test_config |
| 38 | |
Scott Zawalski | 9d7955f | 2012-03-20 20:30:51 -0400 | [diff] [blame] | 39 | # Autotest imports |
| 40 | |
| 41 | import common |
| 42 | |
| 43 | from autotest_lib.client.common_lib.cros import dev_server as new_dev_server |
| 44 | |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 45 | |
| 46 | # Default location of ChromeOS source checkout. |
| 47 | DEFAULT_CROS_PATH = os.path.join('/usr/local/google/home', |
| 48 | os.environ['USER'], 'chromeos/chromeos') |
| 49 | |
| 50 | |
| 51 | class Downloader(object): |
| 52 | """Main class for Downloader. All the magic happens in ProcessBoards().""" |
| 53 | |
| 54 | def __init__(self, options, config): |
| 55 | """Inits Downloader class with options and config data structures. |
| 56 | |
| 57 | Args: |
| 58 | options: Command line options packages as created by ParseOptions(). |
| 59 | config: Dictionary of configuration as loaded from JSON. |
| 60 | """ |
| 61 | self._options = options |
| 62 | self._config = config |
| 63 | |
| 64 | def ProcessBoards(self): |
| 65 | """For each board: find latest build version, create components, and upload. |
| 66 | |
| 67 | The main processing function for the Downloader class. Given a configuration |
| 68 | mapping between boards and locations it will: |
| 69 | |
| 70 | - Find the latest version of a build for a given board. |
| 71 | - Determine if the build already exists on Dev Server. |
| 72 | - Download and extract the build to a staging directory. |
| 73 | - Convert binary testing image into relevant components. |
| 74 | - Upload components to Dev Server. |
| 75 | """ |
| 76 | # Initialize boards listing. If user has specified a board and it's valid, |
| 77 | # only process that board. |
| 78 | boards = self._config['boards'] |
| 79 | if self._options.board and self._options.board in boards: |
| 80 | boards = {self._options.board: boards[self._options.board]} |
| 81 | |
| 82 | # Initialize Dev Server utility class. |
| 83 | dev = dev_server.DevServer(**self._config['dev_server']) |
Scott Zawalski | 9d7955f | 2012-03-20 20:30:51 -0400 | [diff] [blame] | 84 | new_dev = new_dev_server.DevServer() |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 85 | |
| 86 | # Main processing loop. Look for new builds of each board. |
| 87 | for board in boards: |
Scott Zawalski | 9d7955f | 2012-03-20 20:30:51 -0400 | [diff] [blame] | 88 | # |board| is the same as target in the new nomenclature, i.e. |
| 89 | # x86-alex-release. this also uses old style; R18, R16, etc. |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 90 | board_cfg = boards[board] |
| 91 | board_cfg.setdefault('archive_path', None) |
| 92 | board_cfg.setdefault('build_pattern', None) |
| 93 | board_cfg.setdefault('boto', None) |
| 94 | board_cfg.setdefault('import_tests', False) |
Scott Zawalski | 9d7955f | 2012-03-20 20:30:51 -0400 | [diff] [blame] | 95 | if not board_cfg.get('archive_server'): |
| 96 | logging.info('Skipping %s, devserver handles the download.', board) |
| 97 | continue |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 98 | |
| 99 | # Bind remote_dir and staging_dir here so we can tell if we need to do any |
| 100 | # cleanup after an exception occurs before remote_dir is set. |
| 101 | remote_dir = staging_dir = None |
| 102 | try: |
| 103 | logging.info('------------[ Processing board %s ]------------', board) |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 104 | # Retrieve the latest build version for this board. |
| 105 | if not self._options.build: |
Aviv Keshet | 7f76a16 | 2013-02-13 17:22:00 -0800 | [diff] [blame] | 106 | |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 107 | build = build_util.GetLatestBuildbotBuildVersion( |
| 108 | archive_server=board_cfg['archive_server'], board=board, |
| 109 | boto=board_cfg['boto'], archive_path=board_cfg['archive_path'], |
| 110 | build_pattern=board_cfg['build_pattern']) |
| 111 | |
| 112 | if not build: |
| 113 | logging.info('Bad build version returned from server. Skipping.') |
| 114 | continue |
| 115 | |
| 116 | logging.info('Latest build available on Buildbot is %s .', build) |
| 117 | else: |
| 118 | build = self._options.build |
| 119 | |
Scott Zawalski | 9d7955f | 2012-03-20 20:30:51 -0400 | [diff] [blame] | 120 | if board_cfg.get('download_devserver'): |
| 121 | # Use new dev server download pathway for staging image. |
| 122 | image = '%s/%s' % (board, build) |
| 123 | logging.info('Downloading %s using the dev server.', image) |
| 124 | new_dev.trigger_download(image) |
| 125 | continue |
| 126 | |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 127 | # Create Dev Server directory for this build and tell other Downloader |
| 128 | # instances we're working on this build. |
| 129 | try: |
| 130 | remote_dir = dev.AcquireLock('/'.join([board, build])) |
| 131 | except common_util.ChromeOSTestError: |
| 132 | # Label as info instead of error because this will be the most common |
| 133 | # end point for the majority of runs. |
| 134 | logging.info('Refused lock for build. Assuming build has already been' |
| 135 | ' processed.') |
| 136 | continue |
| 137 | |
| 138 | # Download and extract build to a temporary directory or process the |
| 139 | # build at the user specified staging directory. |
| 140 | if not self._options.staging: |
| 141 | logging.info('Downloading build from %s/%s', |
| 142 | board_cfg['archive_server'], board) |
| 143 | |
| 144 | staging_dir, archive_path = build_util.DownloadAndExtractBuild( |
| 145 | archive_server=board_cfg['archive_server'], |
| 146 | archive_path=board_cfg['archive_path'], board=board, |
| 147 | boto=board_cfg['boto'], build=build) |
| 148 | |
| 149 | else: |
| 150 | staging_dir = self._options.staging |
| 151 | |
| 152 | # Do we need to import tests? |
| 153 | if board_cfg['import_tests'] and not autotest_util.ImportTests( |
| 154 | hosts=self._config['import_hosts'], staging_dir=staging_dir): |
| 155 | logging.warning('One or more hosts failed to import tests!') |
| 156 | |
| 157 | # Process build and create update.gz and stateful.image.gz |
| 158 | logging.info('Creating build components under %s', staging_dir) |
| 159 | build_util.CreateBuildComponents( |
| 160 | staging_dir=staging_dir, cros_checkout=self._options.cros_checkout) |
| 161 | |
| 162 | # Generate N->N AU payload. |
| 163 | nton_payload_dir = None |
| 164 | try: |
| 165 | nton_payload_dir = os.path.join(dev.AU_BASE, build + '_nton') |
| 166 | common_util.MakedirsExisting( |
| 167 | os.path.join(staging_dir, nton_payload_dir)) |
| 168 | |
| 169 | build_util.CreateUpdateZip( |
| 170 | cros_checkout=self._options.cros_checkout, |
| 171 | staging_dir=staging_dir, output_dir=nton_payload_dir, |
| 172 | source_image=build_util.TEST_IMAGE) |
| 173 | except common_util.ChromeOSTestError, e: |
| 174 | if nton_payload_dir: |
| 175 | shutil.rmtree(os.path.join(staging_dir, nton_payload_dir)) |
| 176 | logging.exception(e) |
| 177 | |
| 178 | # Generate N-1->N AU payload. |
| 179 | mton_payload_dir = None |
| 180 | try: |
| 181 | # Retrieve N-1 (current LATEST) build from Dev Server. |
Aviv Keshet | 7f76a16 | 2013-02-13 17:22:00 -0800 | [diff] [blame] | 182 | |
| 183 | |
| 184 | raise NotImplementedException('This code is broken. Do not use.' |
| 185 | 'If you must use, contact the lab ' |
| 186 | 'team.') |
| 187 | # ..because the following function call no longer exists |
| 188 | # previous_build = dev.GetLatestBuildVersion(board) |
| 189 | |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 190 | previous_image = dev.GetImage(board, previous_build, staging_dir) |
| 191 | |
| 192 | mton_payload_dir = os.path.join(dev.AU_BASE, previous_build + '_mton') |
| 193 | common_util.MakedirsExisting( |
| 194 | os.path.join(staging_dir, mton_payload_dir)) |
| 195 | |
| 196 | build_util.CreateUpdateZip( |
| 197 | cros_checkout=self._options.cros_checkout, |
| 198 | staging_dir=staging_dir, output_dir=mton_payload_dir, |
| 199 | source_image=previous_image) |
| 200 | except common_util.ChromeOSTestError, e: |
| 201 | if mton_payload_dir: |
| 202 | shutil.rmtree(os.path.join(staging_dir, mton_payload_dir)) |
| 203 | logging.exception(e) |
| 204 | |
| 205 | # TODO(dalecurtis): Sync official chromeos_test_image.bins. |
| 206 | |
| 207 | # TODO(dalecurtis): Generate <official>->N AU payloads. |
| 208 | |
| 209 | # Upload required components into jailed Dev Server. |
| 210 | logging.info('Uploading build components to Dev Server.') |
| 211 | dev.UploadBuildComponents(staging_dir=staging_dir, upload_image=True, |
| 212 | remote_dir=remote_dir) |
| 213 | |
| 214 | # Create and upload LATEST file to the Dev Server. |
| 215 | if not self._options.build: |
| 216 | dev.UpdateLatestBuild(board=board, build=build) |
| 217 | |
| 218 | #TODO(dalecurtis): Disabled, since it's not under active development. |
| 219 | #appengine_cfg = self._config.get('appengine', {}) |
| 220 | #if appengine_cfg: |
| 221 | # dash_util.UploadBuild(appengine_cfg, board, build, archive_path) |
| 222 | else: |
| 223 | logging.warning('LATEST file not updated because --build was ' |
| 224 | 'specified. Make sure you manually update the LATEST ' |
| 225 | 'file if required.') |
| 226 | except Exception, e: |
| 227 | logging.exception(e) |
| 228 | |
| 229 | # Release processing lock, which will remove build components directory |
| 230 | # so future runs can retry. |
| 231 | if remote_dir: |
| 232 | try: |
| 233 | dev.ReleaseLock('/'.join([board, build])) |
| 234 | except (KeyboardInterrupt, common_util.ChromeOSTestError): |
| 235 | logging.critical('Failed to clean up Dev Server after failed run on' |
| 236 | ' build %s.', build) |
| 237 | |
| 238 | # If Exception was a ^C, break out of processing loop. |
| 239 | if isinstance(e, KeyboardInterrupt): |
| 240 | break |
| 241 | if not isinstance(e, common_util.ChromeOSTestError): |
| 242 | raise |
| 243 | finally: |
| 244 | # Always cleanup after ourselves. As an automated system with firm |
| 245 | # inputs, it's trivial to recreate error conditions manually. Where as |
| 246 | # repeated failures over a long weekend could bring the system down. |
| 247 | if staging_dir: |
| 248 | # Remove the staging directory. |
| 249 | logging.info('Cleaning up staging directory %s', staging_dir) |
| 250 | cmd = 'sudo rm -rf ' + staging_dir |
| 251 | msg = 'Failed to clean up staging directory!' |
| 252 | common_util.RunCommand(cmd=cmd, error_msg=msg) |
| 253 | |
| 254 | |
| 255 | def ParseOptions(): |
| 256 | """Parse command line options. Returns 2-tuple of options and config.""" |
| 257 | # If default config exists, parse it and use values for help screen. |
| 258 | config = test_config.TestConfig() |
| 259 | |
| 260 | # If config is provided parse values to make help screen more useful. |
| 261 | boards = config.ParseConfigGroups()[0] |
| 262 | |
| 263 | parser = optparse.OptionParser('usage: %prog [options]') |
| 264 | |
| 265 | parser.add_option('--board', dest='board', |
| 266 | help='Process only the specified board. Valid boards: %s' |
| 267 | % boards) |
| 268 | parser.add_option('--build', dest='build', |
| 269 | help=('Specify the build version to process. Must be used ' |
| 270 | 'with the --board option. LATEST file will not be ' |
| 271 | 'updated with this option.')) |
| 272 | parser.add_option('--cros_checkout', dest='cros_checkout', |
| 273 | default=DEFAULT_CROS_PATH, |
| 274 | help=('Location of ChromeOS source checkout. Defaults to ' |
| 275 | '"%default".')) |
| 276 | parser.add_option('--staging', dest='staging', |
| 277 | help=('Specify a pre-populated staging directory. Must be ' |
| 278 | 'used with the --board and --build options. Useful ' |
| 279 | 'to finish a run that was interrupted or failed.')) |
| 280 | |
| 281 | # Add utility/helper class command line options. |
| 282 | test_config.AddOptions(parser) |
| 283 | log_util.AddOptions(parser) |
| 284 | |
| 285 | options = parser.parse_args()[0] |
| 286 | |
| 287 | if options.build and not options.board: |
| 288 | parser.error('If --build is used, --board must be specified as well.') |
| 289 | |
| 290 | if options.staging and not (options.board and options.build): |
| 291 | parser.error(('If --staging is used, --board and --build must be' |
| 292 | ' specified as well.')) |
| 293 | |
| 294 | # Load correct config file if alternate is specified. |
| 295 | if options.config != test_config.DEFAULT_CONFIG_FILE: |
| 296 | config = test_config.TestConfig(options.config) |
| 297 | boards = config.ParseConfigGroups()[0] |
| 298 | |
| 299 | if options.board and not options.board in boards: |
| 300 | parser.error('Invalid board "%s" specified. Valid boards are: %s' |
| 301 | % (options.board, boards)) |
| 302 | |
| 303 | return options, config.GetConfig() |
| 304 | |
| 305 | |
| 306 | def main(): |
| 307 | # Parse options and load config. |
| 308 | options, config = ParseOptions() |
| 309 | |
| 310 | # Setup logger and enable verbose mode if specified. |
| 311 | log_util.InitializeLogging(options.verbose) |
| 312 | |
| 313 | Downloader(options=options, config=config).ProcessBoards() |
| 314 | |
| 315 | |
| 316 | if __name__ == '__main__': |
| 317 | main() |