blob: d719a2bda1e6fdb217a76a1677275cfc7ba2730b [file] [log] [blame]
Mike Frysingerd13faeb2013-09-05 16:00:46 -04001# Copyright (c) 2013 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"""ChromeOS image pusher (from cbuildbot to signer).
6
7This pushes files from the archive bucket to the signer bucket and marks
8artifacts for signing (which a signing process will look for).
9"""
10
11from __future__ import print_function
12
13import ConfigParser
14import cStringIO
Don Garrettfd97b402015-09-03 00:59:21 +000015import errno
Mike Frysingerd13faeb2013-09-05 16:00:46 -040016import getpass
17import os
18import re
19import tempfile
Mike Frysinger09fe0122014-02-09 02:44:05 -050020import textwrap
Mike Frysingerd13faeb2013-09-05 16:00:46 -040021
Don Garrett88b8d782014-05-13 17:30:55 -070022from chromite.cbuildbot import constants
Mike Frysingerd13faeb2013-09-05 16:00:46 -040023from chromite.lib import commandline
24from chromite.lib import cros_build_lib
Ralph Nathan5a582ff2015-03-20 18:18:30 -070025from chromite.lib import cros_logging as logging
Mike Frysingerd13faeb2013-09-05 16:00:46 -040026from chromite.lib import gs
27from chromite.lib import osutils
28from chromite.lib import signing
29
30
31# This will split a fully qualified ChromeOS version string up.
32# R34-5126.0.0 will break into "34" and "5126.0.0".
33VERSION_REGEX = r'^R([0-9]+)-([^-]+)'
34
Mike Frysingerdad40d62014-02-09 02:18:02 -050035# The test signers will scan this dir looking for test work.
36# Keep it in sync with the signer config files [gs_test_buckets].
37TEST_SIGN_BUCKET_BASE = 'gs://chromeos-throw-away-bucket/signer-tests'
38
David Rileyf8205122015-09-04 13:46:36 -070039# Keysets that are only valid in the above test bucket.
40TEST_KEYSET_PREFIX = 'test-keys'
41TEST_KEYSETS = set((
42 'mp',
43 'premp',
44 'nvidia-premp',
45))
Mike Frysingerdad40d62014-02-09 02:18:02 -050046
Mike Frysingerd13faeb2013-09-05 16:00:46 -040047
Mike Frysinger4495b032014-03-05 17:24:03 -050048class PushError(Exception):
49 """When an (unknown) error happened while trying to push artifacts."""
50
51
Mike Frysingerd13faeb2013-09-05 16:00:46 -040052class MissingBoardInstructions(Exception):
53 """Raised when a board lacks any signer instructions."""
54
55
56class InputInsns(object):
57 """Object to hold settings for a signable board.
58
59 Note: The format of the instruction file pushimage outputs (and the signer
60 reads) is not exactly the same as the instruction file pushimage reads.
61 """
62
63 def __init__(self, board):
64 self.board = board
65
66 config = ConfigParser.ConfigParser()
67 config.readfp(open(self.GetInsnFile('DEFAULT')))
Don Garrettfd97b402015-09-03 00:59:21 +000068 try:
69 input_insn = self.GetInsnFile('recovery')
70 config.readfp(open(input_insn))
71 except IOError as e:
72 if e.errno == errno.ENOENT:
73 # This board doesn't have any signing instructions.
74 # This is normal for new or experimental boards.
75 raise MissingBoardInstructions(input_insn)
76 raise
Mike Frysingerd13faeb2013-09-05 16:00:46 -040077 self.cfg = config
78
79 def GetInsnFile(self, image_type):
80 """Find the signer instruction files for this board/image type.
81
82 Args:
83 image_type: The type of instructions to load. It can be a common file
84 (like "DEFAULT"), or one of the --sign-types.
85
86 Returns:
87 Full path to the instruction file using |image_type| and |self.board|.
88 """
89 if image_type == image_type.upper():
90 name = image_type
Don Garrettfd97b402015-09-03 00:59:21 +000091 elif image_type == 'recovery':
Mike Frysingerd13faeb2013-09-05 16:00:46 -040092 name = self.board
93 else:
94 name = '%s.%s' % (self.board, image_type)
95
96 return os.path.join(signing.INPUT_INSN_DIR, '%s.instructions' % name)
97
98 @staticmethod
99 def SplitCfgField(val):
100 """Split a string into multiple elements.
101
102 This centralizes our convention for multiple elements in the input files
103 being delimited by either a space or comma.
104
105 Args:
106 val: The string to split.
107
108 Returns:
109 The list of elements from having done split the string.
110 """
111 return val.replace(',', ' ').split()
112
113 def GetChannels(self):
114 """Return the list of channels to sign for this board.
115
116 If the board-specific config doesn't specify a preference, we'll use the
117 common settings.
118 """
119 return self.SplitCfgField(self.cfg.get('insns', 'channel'))
120
121 def GetKeysets(self):
122 """Return the list of keysets to sign for this board."""
123 return self.SplitCfgField(self.cfg.get('insns', 'keyset'))
124
125 def OutputInsns(self, image_type, output_file, sect_insns, sect_general):
126 """Generate the output instruction file for sending to the signer.
127
128 Note: The format of the instruction file pushimage outputs (and the signer
129 reads) is not exactly the same as the instruction file pushimage reads.
130
131 Args:
132 image_type: The type of image we will be signing (see --sign-types).
133 output_file: The file to write the new instruction file to.
134 sect_insns: Items to set/override in the [insns] section.
135 sect_general: Items to set/override in the [general] section.
136 """
137 config = ConfigParser.ConfigParser()
138 config.readfp(open(self.GetInsnFile(image_type)))
139
140 # Clear channel entry in instructions file, ensuring we only get
141 # one channel for the signer to look at. Then provide all the
142 # other details for this signing request to avoid any ambiguity
143 # and to avoid relying on encoding data into filenames.
144 for sect, fields in zip(('insns', 'general'), (sect_insns, sect_general)):
145 if not config.has_section(sect):
146 config.add_section(sect)
147 for k, v in fields.iteritems():
148 config.set(sect, k, v)
149
150 output = cStringIO.StringIO()
151 config.write(output)
152 data = output.getvalue()
153 osutils.WriteFile(output_file, data)
Ralph Nathan5a582ff2015-03-20 18:18:30 -0700154 logging.debug('generated insns file for %s:\n%s', image_type, data)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400155
156
157def MarkImageToBeSigned(ctx, tbs_base, insns_path, priority):
158 """Mark an instructions file for signing.
159
160 This will upload a file to the GS bucket flagging an image for signing by
161 the signers.
162
163 Args:
164 ctx: A viable gs.GSContext.
165 tbs_base: The full path to where the tobesigned directory lives.
166 insns_path: The path (relative to |tbs_base|) of the file to sign.
167 priority: Set the signing priority (lower == higher prio).
168
169 Returns:
170 The full path to the remote tobesigned file.
171 """
172 if priority < 0 or priority > 99:
173 raise ValueError('priority must be [0, 99] inclusive')
174
175 if insns_path.startswith(tbs_base):
176 insns_path = insns_path[len(tbs_base):].lstrip('/')
177
178 tbs_path = '%s/tobesigned/%02i,%s' % (tbs_base, priority,
179 insns_path.replace('/', ','))
180
Mike Frysinger6430d132014-10-27 23:43:30 -0400181 # The caller will catch gs.GSContextException for us.
182 ctx.Copy('-', tbs_path, input=cros_build_lib.MachineDetails())
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400183
184 return tbs_path
185
186
187def PushImage(src_path, board, versionrev=None, profile=None, priority=50,
Mike Frysingerdad40d62014-02-09 02:18:02 -0500188 sign_types=None, dry_run=False, mock=False, force_keysets=()):
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400189 """Push the image from the archive bucket to the release bucket.
190
191 Args:
192 src_path: Where to copy the files from; can be a local path or gs:// URL.
193 Should be a full path to the artifacts in either case.
194 board: The board we're uploading artifacts for (e.g. $BOARD).
195 versionrev: The full Chromium OS version string (e.g. R34-5126.0.0).
196 profile: The board profile in use (e.g. "asan").
197 priority: Set the signing priority (lower == higher prio).
198 sign_types: If set, a set of types which we'll restrict ourselves to
199 signing. See the --sign-types option for more details.
200 dry_run: Show what would be done, but do not upload anything.
201 mock: Upload to a testing bucket rather than the real one.
Mike Frysingerdad40d62014-02-09 02:18:02 -0500202 force_keysets: Set of keysets to use rather than what the inputs say.
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400203
204 Returns:
Don Garrett9459c2f2014-01-22 18:20:24 -0800205 A dictionary that maps 'channel' -> ['gs://signer_instruction_uri1',
206 'gs://signer_instruction_uri2',
207 ...]
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400208 """
Mike Frysinger4495b032014-03-05 17:24:03 -0500209 # Whether we hit an unknown error. If so, we'll throw an error, but only
210 # at the end (so that we still upload as many files as possible).
Don Garrettfd97b402015-09-03 00:59:21 +0000211 unknown_error = False
Mike Frysinger4495b032014-03-05 17:24:03 -0500212
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400213 if versionrev is None:
214 # Extract milestone/version from the directory name.
215 versionrev = os.path.basename(src_path)
216
217 # We only support the latest format here. Older releases can use pushimage
218 # from the respective branch which deals with legacy cruft.
219 m = re.match(VERSION_REGEX, versionrev)
220 if not m:
221 raise ValueError('version %s does not match %s' %
222 (versionrev, VERSION_REGEX))
223 milestone = m.group(1)
224 version = m.group(2)
225
226 # Normalize board to always use dashes not underscores. This is mostly a
227 # historical artifact at this point, but we can't really break it since the
228 # value is used in URLs.
229 boardpath = board.replace('_', '-')
230 if profile is not None:
231 boardpath += '-%s' % profile.replace('_', '-')
232
233 ctx = gs.GSContext(dry_run=dry_run)
234
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400235 try:
236 input_insns = InputInsns(board)
237 except MissingBoardInstructions as e:
Ralph Nathan446aee92015-03-23 14:44:56 -0700238 logging.warning('board "%s" is missing base instruction file: %s', board, e)
239 logging.warning('not uploading anything for signing')
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400240 return
241 channels = input_insns.GetChannels()
Mike Frysingerdad40d62014-02-09 02:18:02 -0500242
243 # We want force_keysets as a set, and keysets as a list.
244 force_keysets = set(force_keysets)
245 keysets = list(force_keysets) if force_keysets else input_insns.GetKeysets()
246
247 if mock:
Ralph Nathan03047282015-03-23 11:09:32 -0700248 logging.info('Upload mode: mock; signers will not process anything')
Mike Frysingerdad40d62014-02-09 02:18:02 -0500249 tbs_base = gs_base = os.path.join(constants.TRASH_BUCKET, 'pushimage-tests',
250 getpass.getuser())
David Rileyf8205122015-09-04 13:46:36 -0700251 elif set(['%s-%s' % (TEST_KEYSET_PREFIX, x)
252 for x in TEST_KEYSETS]) & force_keysets:
Ralph Nathan03047282015-03-23 11:09:32 -0700253 logging.info('Upload mode: test; signers will process test keys')
Mike Frysingerdad40d62014-02-09 02:18:02 -0500254 # We need the tbs_base to be in the place the signer will actually scan.
255 tbs_base = TEST_SIGN_BUCKET_BASE
256 gs_base = os.path.join(tbs_base, getpass.getuser())
257 else:
Ralph Nathan03047282015-03-23 11:09:32 -0700258 logging.info('Upload mode: normal; signers will process the images')
Mike Frysingerdad40d62014-02-09 02:18:02 -0500259 tbs_base = gs_base = constants.RELEASE_BUCKET
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400260
261 sect_general = {
262 'config_board': board,
263 'board': boardpath,
264 'version': version,
265 'versionrev': versionrev,
266 'milestone': milestone,
267 }
268 sect_insns = {}
269
270 if dry_run:
Ralph Nathan03047282015-03-23 11:09:32 -0700271 logging.info('DRY RUN MODE ACTIVE: NOTHING WILL BE UPLOADED')
272 logging.info('Signing for channels: %s', ' '.join(channels))
273 logging.info('Signing for keysets : %s', ' '.join(keysets))
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400274
Don Garrett9459c2f2014-01-22 18:20:24 -0800275 instruction_urls = {}
276
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400277 def _ImageNameBase(image_type=None):
278 lmid = ('%s-' % image_type) if image_type else ''
279 return 'ChromeOS-%s%s-%s' % (lmid, versionrev, boardpath)
280
281 for channel in channels:
Ralph Nathan5a582ff2015-03-20 18:18:30 -0700282 logging.debug('\n\n#### CHANNEL: %s ####\n', channel)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400283 sect_insns['channel'] = channel
284 sub_path = '%s-channel/%s/%s' % (channel, boardpath, version)
285 dst_path = '%s/%s' % (gs_base, sub_path)
Ralph Nathan03047282015-03-23 11:09:32 -0700286 logging.info('Copying images to %s', dst_path)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400287
Don Garrettfd97b402015-09-03 00:59:21 +0000288 recovery_base = _ImageNameBase('recovery')
289 factory_base = _ImageNameBase('factory')
290 firmware_base = _ImageNameBase('firmware')
291 test_base = _ImageNameBase('test')
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400292 hwqual_tarball = 'chromeos-hwqual-%s-%s.tar.bz2' % (board, versionrev)
293
Don Garrettfd97b402015-09-03 00:59:21 +0000294 # Upload all the files first before flagging them for signing.
295 files_to_copy = (
296 # pylint: disable=bad-whitespace
297 # <src> <dst>
298 # <signing type> <sfx>
299 ('recovery_image.tar.xz', recovery_base, 'tar.xz',
300 'recovery'),
301
302 ('factory_image.zip', factory_base, 'zip',
303 'factory'),
304
305 ('firmware_from_source.tar.bz2', firmware_base, 'tar.bz2',
306 'firmware'),
307
308 ('image.zip', _ImageNameBase(), 'zip', ''),
309 ('chromiumos_test_image.tar.xz', test_base, 'tar.xz', ''),
310 ('debug.tgz', 'debug-%s' % boardpath, 'tgz', ''),
311 (hwqual_tarball, '', '', ''),
312 ('au-generator.zip', '', '', ''),
313 ('stateful.tgz', '', '', ''),
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400314 )
Don Garrettfd97b402015-09-03 00:59:21 +0000315 files_to_sign = []
316 for src, dst, sfx, image_type in files_to_copy:
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400317 if not dst:
318 dst = src
Don Garrettfd97b402015-09-03 00:59:21 +0000319 elif sfx:
320 dst += '.%s' % sfx
Mike Frysingere51a2652014-01-18 02:36:16 -0500321 try:
322 ctx.Copy(os.path.join(src_path, src), os.path.join(dst_path, dst))
323 except gs.GSNoSuchKey:
Ralph Nathan446aee92015-03-23 14:44:56 -0700324 logging.warning('Skipping %s as it does not exist', src)
Don Garrettfd97b402015-09-03 00:59:21 +0000325 continue
Mike Frysinger4495b032014-03-05 17:24:03 -0500326 except gs.GSContextException:
Don Garrettfd97b402015-09-03 00:59:21 +0000327 unknown_error = True
Ralph Nathan59900422015-03-24 10:41:17 -0700328 logging.error('Skipping %s due to unknown GS error', src, exc_info=True)
Don Garrettfd97b402015-09-03 00:59:21 +0000329 continue
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400330
Don Garrettfd97b402015-09-03 00:59:21 +0000331 if image_type:
332 dst_base = dst[:-(len(sfx) + 1)]
333 assert dst == '%s.%s' % (dst_base, sfx)
334 files_to_sign += [[image_type, dst_base, '.%s' % sfx]]
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400335
336 # Now go through the subset for signing.
337 for keyset in keysets:
Ralph Nathan5a582ff2015-03-20 18:18:30 -0700338 logging.debug('\n\n#### KEYSET: %s ####\n', keyset)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400339 sect_insns['keyset'] = keyset
340 for image_type, dst_name, suffix in files_to_sign:
Don Garrettfd97b402015-09-03 00:59:21 +0000341 dst_archive = '%s%s' % (dst_name, suffix)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400342 sect_general['archive'] = dst_archive
343 sect_general['type'] = image_type
344
Don Garrettfd97b402015-09-03 00:59:21 +0000345 # See if the caller has requested we only sign certain types.
346 if sign_types:
347 if not image_type in sign_types:
348 logging.info('Skipping %s signing as it was not requested',
349 image_type)
350 continue
351 else:
352 # In the default/automatic mode, only flag files for signing if the
353 # archives were actually uploaded in a previous stage.
354 gs_artifact_path = os.path.join(dst_path, dst_archive)
355 try:
356 exists = ctx.Exists(gs_artifact_path)
357 except gs.GSContextException:
358 unknown_error = True
359 exists = False
360 logging.error('Unknown error while checking %s', gs_artifact_path,
361 exc_info=True)
362 if not exists:
363 logging.info('%s does not exist. Nothing to sign.',
364 gs_artifact_path)
365 continue
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400366
367 input_insn_path = input_insns.GetInsnFile(image_type)
368 if not os.path.exists(input_insn_path):
Ralph Nathan03047282015-03-23 11:09:32 -0700369 logging.info('%s does not exist. Nothing to sign.', input_insn_path)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400370 continue
371
372 # Generate the insn file for this artifact that the signer will use,
373 # and flag it for signing.
374 with tempfile.NamedTemporaryFile(
375 bufsize=0, prefix='pushimage.insns.') as insns_path:
376 input_insns.OutputInsns(image_type, insns_path.name, sect_insns,
377 sect_general)
378
379 gs_insns_path = '%s/%s' % (dst_path, dst_name)
380 if keyset != keysets[0]:
381 gs_insns_path += '-%s' % keyset
382 gs_insns_path += '.instructions'
383
Mike Frysinger4495b032014-03-05 17:24:03 -0500384 try:
385 ctx.Copy(insns_path.name, gs_insns_path)
386 except gs.GSContextException:
Don Garrettfd97b402015-09-03 00:59:21 +0000387 unknown_error = True
Ralph Nathan59900422015-03-24 10:41:17 -0700388 logging.error('Unknown error while uploading insns %s',
389 gs_insns_path, exc_info=True)
Mike Frysinger4495b032014-03-05 17:24:03 -0500390 continue
391
392 try:
393 MarkImageToBeSigned(ctx, tbs_base, gs_insns_path, priority)
394 except gs.GSContextException:
Don Garrettfd97b402015-09-03 00:59:21 +0000395 unknown_error = True
Ralph Nathan59900422015-03-24 10:41:17 -0700396 logging.error('Unknown error while marking for signing %s',
397 gs_insns_path, exc_info=True)
Mike Frysinger4495b032014-03-05 17:24:03 -0500398 continue
Ralph Nathan03047282015-03-23 11:09:32 -0700399 logging.info('Signing %s image %s', image_type, gs_insns_path)
Don Garrett9459c2f2014-01-22 18:20:24 -0800400 instruction_urls.setdefault(channel, []).append(gs_insns_path)
401
Don Garrettfd97b402015-09-03 00:59:21 +0000402 if unknown_error:
Mike Frysinger4495b032014-03-05 17:24:03 -0500403 raise PushError('hit some unknown error(s)', instruction_urls)
404
Don Garrett9459c2f2014-01-22 18:20:24 -0800405 return instruction_urls
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400406
407
408def main(argv):
409 parser = commandline.ArgumentParser(description=__doc__)
410
411 # The type of image_dir will strip off trailing slashes (makes later
412 # processing simpler and the display prettier).
413 parser.add_argument('image_dir', default=None, type='local_or_gs_path',
414 help='full path of source artifacts to upload')
415 parser.add_argument('--board', default=None, required=True,
416 help='board to generate symbols for')
417 parser.add_argument('--profile', default=None,
418 help='board profile in use (e.g. "asan")')
419 parser.add_argument('--version', default=None,
420 help='version info (normally extracted from image_dir)')
421 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
422 help='show what would be done, but do not upload')
423 parser.add_argument('-M', '--mock', default=False, action='store_true',
424 help='upload things to a testing bucket (dev testing)')
David Rileyf8205122015-09-04 13:46:36 -0700425 parser.add_argument('--test-sign', default=[], action='append',
426 choices=TEST_KEYSETS,
427 help='mung signing behavior to sign w/ test keys')
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400428 parser.add_argument('--priority', type=int, default=50,
429 help='set signing priority (lower == higher prio)')
430 parser.add_argument('--sign-types', default=None, nargs='+',
Don Garrettfd97b402015-09-03 00:59:21 +0000431 choices=('recovery', 'factory', 'firmware'),
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400432 help='only sign specified image types')
Mike Frysinger09fe0122014-02-09 02:44:05 -0500433 parser.add_argument('--yes', action='store_true', default=False,
434 help='answer yes to all prompts')
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400435
436 opts = parser.parse_args(argv)
437 opts.Freeze()
438
David Rileyf8205122015-09-04 13:46:36 -0700439 force_keysets = set(['%s-%s' % (TEST_KEYSET_PREFIX, x)
440 for x in opts.test_sign])
Mike Frysingerdad40d62014-02-09 02:18:02 -0500441
Mike Frysinger09fe0122014-02-09 02:44:05 -0500442 # If we aren't using mock or test or dry run mode, then let's prompt the user
443 # to make sure they actually want to do this. It's rare that people want to
444 # run this directly and hit the release bucket.
445 if not (opts.mock or force_keysets or opts.dry_run) and not opts.yes:
446 prolog = '\n'.join(textwrap.wrap(textwrap.dedent(
447 'Uploading images for signing to the *release* bucket is not something '
448 'you generally should be doing yourself.'), 80)).strip()
449 if not cros_build_lib.BooleanPrompt(
450 prompt='Are you sure you want to sign these images',
451 default=False, prolog=prolog):
452 cros_build_lib.Die('better safe than sorry')
453
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400454 PushImage(opts.image_dir, opts.board, versionrev=opts.version,
455 profile=opts.profile, priority=opts.priority,
Mike Frysingerdad40d62014-02-09 02:18:02 -0500456 sign_types=opts.sign_types, dry_run=opts.dry_run, mock=opts.mock,
457 force_keysets=force_keysets)