blob: 85ca92b2f51599ea0c277714df03bfbe6799a84a [file] [log] [blame]
Scott Zawalski20a9b582011-11-21 11:49:40 -08001#!/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
9Downloader is a tool for downloading and processing images for the various board
10types supported by ChromeOS.
11
12All downloading and processing is driven by a board to archive server mapping in
13a specified JSON config file. Boards are processed sequentially.
14
15Downloader is multi-instance friendly. You can spin up as many instances as
16necessary to handle image processing load (which can be substantial). It is not
17recommended to run more than one instance per machine.
18
19Downloader expects the JSON config file to be in the current working directory
20or to be run with --config pointing to the actual config file.
21"""
22
23__author__ = 'dalecurtis@google.com (Dale Curtis)'
24
25import logging
26import optparse
27import os
28import re
29import shutil
30
31from chromeos_test import autotest_util
32from chromeos_test import build_util
33from chromeos_test import common_util
34from chromeos_test import dash_util
35from chromeos_test import dev_server
36from chromeos_test import log_util
37from chromeos_test import test_config
38
Scott Zawalski9d7955f2012-03-20 20:30:51 -040039# Autotest imports
40
41import common
42
43from autotest_lib.client.common_lib.cros import dev_server as new_dev_server
44
Scott Zawalski20a9b582011-11-21 11:49:40 -080045
46# Default location of ChromeOS source checkout.
47DEFAULT_CROS_PATH = os.path.join('/usr/local/google/home',
48 os.environ['USER'], 'chromeos/chromeos')
49
50
51class 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 Zawalski9d7955f2012-03-20 20:30:51 -040084 new_dev = new_dev_server.DevServer()
Scott Zawalski20a9b582011-11-21 11:49:40 -080085
86 # Main processing loop. Look for new builds of each board.
87 for board in boards:
Scott Zawalski9d7955f2012-03-20 20:30:51 -040088 # |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 Zawalski20a9b582011-11-21 11:49:40 -080090 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 Zawalski9d7955f2012-03-20 20:30:51 -040095 if not board_cfg.get('archive_server'):
96 logging.info('Skipping %s, devserver handles the download.', board)
97 continue
Scott Zawalski20a9b582011-11-21 11:49:40 -080098
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 Zawalski20a9b582011-11-21 11:49:40 -0800104 # Retrieve the latest build version for this board.
105 if not self._options.build:
Aviv Keshet7f76a162013-02-13 17:22:00 -0800106
Scott Zawalski20a9b582011-11-21 11:49:40 -0800107 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 Zawalski9d7955f2012-03-20 20:30:51 -0400120 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 Zawalski20a9b582011-11-21 11:49:40 -0800127 # 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 Keshet7f76a162013-02-13 17:22:00 -0800182
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 Zawalski20a9b582011-11-21 11:49:40 -0800190 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
255def 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
306def 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
316if __name__ == '__main__':
317 main()