blob: 048354a45f26a2fd43ddcc38655518d42590d611 [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
15import errno
16import getpass
17import os
18import re
19import tempfile
20
21from chromite.buildbot import constants
22from chromite.lib import commandline
23from chromite.lib import cros_build_lib
24from chromite.lib import git
25from chromite.lib import gs
26from chromite.lib import osutils
27from chromite.lib import signing
28
29
30# This will split a fully qualified ChromeOS version string up.
31# R34-5126.0.0 will break into "34" and "5126.0.0".
32VERSION_REGEX = r'^R([0-9]+)-([^-]+)'
33
Mike Frysingerdad40d62014-02-09 02:18:02 -050034# The test signers will scan this dir looking for test work.
35# Keep it in sync with the signer config files [gs_test_buckets].
36TEST_SIGN_BUCKET_BASE = 'gs://chromeos-throw-away-bucket/signer-tests'
37
38# Ketsets that are only valid in the above test bucket.
39TEST_KEYSETS = set(('test-keys-mp', 'test-keys-premp'))
40
Mike Frysingerd13faeb2013-09-05 16:00:46 -040041
42class MissingBoardInstructions(Exception):
43 """Raised when a board lacks any signer instructions."""
44
45
46class InputInsns(object):
47 """Object to hold settings for a signable board.
48
49 Note: The format of the instruction file pushimage outputs (and the signer
50 reads) is not exactly the same as the instruction file pushimage reads.
51 """
52
53 def __init__(self, board):
54 self.board = board
55
56 config = ConfigParser.ConfigParser()
57 config.readfp(open(self.GetInsnFile('DEFAULT')))
58 try:
59 input_insn = self.GetInsnFile('recovery')
60 config.readfp(open(input_insn))
61 except IOError as e:
62 if e.errno == errno.ENOENT:
63 # This board doesn't have any signing instructions.
64 # This is normal for new or experimental boards.
65 raise MissingBoardInstructions(input_insn)
66 raise
67 self.cfg = config
68
69 def GetInsnFile(self, image_type):
70 """Find the signer instruction files for this board/image type.
71
72 Args:
73 image_type: The type of instructions to load. It can be a common file
74 (like "DEFAULT"), or one of the --sign-types.
75
76 Returns:
77 Full path to the instruction file using |image_type| and |self.board|.
78 """
79 if image_type == image_type.upper():
80 name = image_type
81 elif image_type == 'recovery':
82 name = self.board
83 else:
84 name = '%s.%s' % (self.board, image_type)
85
86 return os.path.join(signing.INPUT_INSN_DIR, '%s.instructions' % name)
87
88 @staticmethod
89 def SplitCfgField(val):
90 """Split a string into multiple elements.
91
92 This centralizes our convention for multiple elements in the input files
93 being delimited by either a space or comma.
94
95 Args:
96 val: The string to split.
97
98 Returns:
99 The list of elements from having done split the string.
100 """
101 return val.replace(',', ' ').split()
102
103 def GetChannels(self):
104 """Return the list of channels to sign for this board.
105
106 If the board-specific config doesn't specify a preference, we'll use the
107 common settings.
108 """
109 return self.SplitCfgField(self.cfg.get('insns', 'channel'))
110
111 def GetKeysets(self):
112 """Return the list of keysets to sign for this board."""
113 return self.SplitCfgField(self.cfg.get('insns', 'keyset'))
114
115 def OutputInsns(self, image_type, output_file, sect_insns, sect_general):
116 """Generate the output instruction file for sending to the signer.
117
118 Note: The format of the instruction file pushimage outputs (and the signer
119 reads) is not exactly the same as the instruction file pushimage reads.
120
121 Args:
122 image_type: The type of image we will be signing (see --sign-types).
123 output_file: The file to write the new instruction file to.
124 sect_insns: Items to set/override in the [insns] section.
125 sect_general: Items to set/override in the [general] section.
126 """
127 config = ConfigParser.ConfigParser()
128 config.readfp(open(self.GetInsnFile(image_type)))
129
130 # Clear channel entry in instructions file, ensuring we only get
131 # one channel for the signer to look at. Then provide all the
132 # other details for this signing request to avoid any ambiguity
133 # and to avoid relying on encoding data into filenames.
134 for sect, fields in zip(('insns', 'general'), (sect_insns, sect_general)):
135 if not config.has_section(sect):
136 config.add_section(sect)
137 for k, v in fields.iteritems():
138 config.set(sect, k, v)
139
140 output = cStringIO.StringIO()
141 config.write(output)
142 data = output.getvalue()
143 osutils.WriteFile(output_file, data)
144 cros_build_lib.Debug('generated insns file for %s:\n%s', image_type, data)
145
146
147def MarkImageToBeSigned(ctx, tbs_base, insns_path, priority):
148 """Mark an instructions file for signing.
149
150 This will upload a file to the GS bucket flagging an image for signing by
151 the signers.
152
153 Args:
154 ctx: A viable gs.GSContext.
155 tbs_base: The full path to where the tobesigned directory lives.
156 insns_path: The path (relative to |tbs_base|) of the file to sign.
157 priority: Set the signing priority (lower == higher prio).
158
159 Returns:
160 The full path to the remote tobesigned file.
161 """
162 if priority < 0 or priority > 99:
163 raise ValueError('priority must be [0, 99] inclusive')
164
165 if insns_path.startswith(tbs_base):
166 insns_path = insns_path[len(tbs_base):].lstrip('/')
167
168 tbs_path = '%s/tobesigned/%02i,%s' % (tbs_base, priority,
169 insns_path.replace('/', ','))
170
171 with tempfile.NamedTemporaryFile(
172 bufsize=0, prefix='pushimage.tbs.') as temp_tbs_file:
173 lines = [
174 'PROG=%s' % __file__,
175 'USER=%s' % getpass.getuser(),
176 'HOSTNAME=%s' % cros_build_lib.GetHostName(fully_qualified=True),
177 'GIT_REV=%s' % git.RunGit(constants.CHROMITE_DIR, ['rev-parse', 'HEAD'])
178 ]
Mike Frysingere68da372014-02-04 03:10:45 -0500179 osutils.WriteFile(temp_tbs_file.name, '\n'.join(lines))
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400180 ctx.Copy(temp_tbs_file.name, tbs_path)
181
182 return tbs_path
183
184
185def PushImage(src_path, board, versionrev=None, profile=None, priority=50,
Mike Frysingerdad40d62014-02-09 02:18:02 -0500186 sign_types=None, dry_run=False, mock=False, force_keysets=()):
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400187 """Push the image from the archive bucket to the release bucket.
188
189 Args:
190 src_path: Where to copy the files from; can be a local path or gs:// URL.
191 Should be a full path to the artifacts in either case.
192 board: The board we're uploading artifacts for (e.g. $BOARD).
193 versionrev: The full Chromium OS version string (e.g. R34-5126.0.0).
194 profile: The board profile in use (e.g. "asan").
195 priority: Set the signing priority (lower == higher prio).
196 sign_types: If set, a set of types which we'll restrict ourselves to
197 signing. See the --sign-types option for more details.
198 dry_run: Show what would be done, but do not upload anything.
199 mock: Upload to a testing bucket rather than the real one.
Mike Frysingerdad40d62014-02-09 02:18:02 -0500200 force_keysets: Set of keysets to use rather than what the inputs say.
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400201
202 Returns:
Don Garrett9459c2f2014-01-22 18:20:24 -0800203 A dictionary that maps 'channel' -> ['gs://signer_instruction_uri1',
204 'gs://signer_instruction_uri2',
205 ...]
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400206 """
207 if versionrev is None:
208 # Extract milestone/version from the directory name.
209 versionrev = os.path.basename(src_path)
210
211 # We only support the latest format here. Older releases can use pushimage
212 # from the respective branch which deals with legacy cruft.
213 m = re.match(VERSION_REGEX, versionrev)
214 if not m:
215 raise ValueError('version %s does not match %s' %
216 (versionrev, VERSION_REGEX))
217 milestone = m.group(1)
218 version = m.group(2)
219
220 # Normalize board to always use dashes not underscores. This is mostly a
221 # historical artifact at this point, but we can't really break it since the
222 # value is used in URLs.
223 boardpath = board.replace('_', '-')
224 if profile is not None:
225 boardpath += '-%s' % profile.replace('_', '-')
226
227 ctx = gs.GSContext(dry_run=dry_run)
228
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400229 try:
230 input_insns = InputInsns(board)
231 except MissingBoardInstructions as e:
232 cros_build_lib.Warning('board "%s" is missing base instruction file: %s',
233 board, e)
234 cros_build_lib.Warning('not uploading anything for signing')
235 return
236 channels = input_insns.GetChannels()
Mike Frysingerdad40d62014-02-09 02:18:02 -0500237
238 # We want force_keysets as a set, and keysets as a list.
239 force_keysets = set(force_keysets)
240 keysets = list(force_keysets) if force_keysets else input_insns.GetKeysets()
241
242 if mock:
243 cros_build_lib.Info('Upload mode: mock; signers will not process anything')
244 tbs_base = gs_base = os.path.join(constants.TRASH_BUCKET, 'pushimage-tests',
245 getpass.getuser())
246 elif TEST_KEYSETS & force_keysets:
247 cros_build_lib.Info('Upload mode: test; signers will process test keys')
248 # We need the tbs_base to be in the place the signer will actually scan.
249 tbs_base = TEST_SIGN_BUCKET_BASE
250 gs_base = os.path.join(tbs_base, getpass.getuser())
251 else:
252 cros_build_lib.Info('Upload mode: normal; signers will process the images')
253 tbs_base = gs_base = constants.RELEASE_BUCKET
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400254
255 sect_general = {
256 'config_board': board,
257 'board': boardpath,
258 'version': version,
259 'versionrev': versionrev,
260 'milestone': milestone,
261 }
262 sect_insns = {}
263
264 if dry_run:
265 cros_build_lib.Info('DRY RUN MODE ACTIVE: NOTHING WILL BE UPLOADED')
266 cros_build_lib.Info('Signing for channels: %s', ' '.join(channels))
267 cros_build_lib.Info('Signing for keysets : %s', ' '.join(keysets))
268
Don Garrett9459c2f2014-01-22 18:20:24 -0800269 instruction_urls = {}
270
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400271 def _ImageNameBase(image_type=None):
272 lmid = ('%s-' % image_type) if image_type else ''
273 return 'ChromeOS-%s%s-%s' % (lmid, versionrev, boardpath)
274
275 for channel in channels:
276 cros_build_lib.Debug('\n\n#### CHANNEL: %s ####\n', channel)
277 sect_insns['channel'] = channel
278 sub_path = '%s-channel/%s/%s' % (channel, boardpath, version)
279 dst_path = '%s/%s' % (gs_base, sub_path)
280 cros_build_lib.Info('Copying images to %s', dst_path)
281
282 recovery_base = _ImageNameBase('recovery')
283 factory_base = _ImageNameBase('factory')
284 firmware_base = _ImageNameBase('firmware')
285 test_base = _ImageNameBase('test')
286 hwqual_tarball = 'chromeos-hwqual-%s-%s.tar.bz2' % (board, versionrev)
287
288 # Upload all the files first before flagging them for signing.
289 files_to_copy = (
290 # <src> <dst>
291 # <signing type> <sfx>
292 ('recovery_image.tar.xz', recovery_base, 'tar.xz',
293 'recovery'),
294
295 ('factory_image.zip', factory_base, 'zip',
296 'factory'),
297
298 ('firmware_from_source.tar.bz2', firmware_base, 'tar.bz2',
299 'firmware'),
300
301 ('image.zip', _ImageNameBase(), 'zip', ''),
302 ('chromiumos_test_image.tar.xz', test_base, 'tar.xz', ''),
303 ('debug.tgz', 'debug-%s' % boardpath, 'tgz', ''),
304 (hwqual_tarball, '', '', ''),
305 ('au-generator.zip', '', '', ''),
306 )
307 files_to_sign = []
308 for src, dst, sfx, image_type in files_to_copy:
309 if not dst:
310 dst = src
311 elif sfx:
312 dst += '.%s' % sfx
Mike Frysingere51a2652014-01-18 02:36:16 -0500313 try:
314 ctx.Copy(os.path.join(src_path, src), os.path.join(dst_path, dst))
315 except gs.GSNoSuchKey:
316 cros_build_lib.Warning('Skipping %s as it does not exist', src)
317 continue
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400318
319 if image_type:
320 dst_base = dst[:-(len(sfx) + 1)]
321 assert dst == '%s.%s' % (dst_base, sfx)
322 files_to_sign += [[image_type, dst_base, '.%s' % sfx]]
323
324 # Now go through the subset for signing.
325 for keyset in keysets:
326 cros_build_lib.Debug('\n\n#### KEYSET: %s ####\n', keyset)
327 sect_insns['keyset'] = keyset
328 for image_type, dst_name, suffix in files_to_sign:
329 dst_archive = '%s%s' % (dst_name, suffix)
330 sect_general['archive'] = dst_archive
331 sect_general['type'] = image_type
332
333 # See if the caller has requested we only sign certain types.
334 if sign_types:
335 if not image_type in sign_types:
336 cros_build_lib.Info('Skipping %s signing as it was not requested',
337 image_type)
338 continue
339 else:
340 # In the default/automatic mode, only flag files for signing if the
341 # archives were actually uploaded in a previous stage.
342 gs_artifact_path = os.path.join(dst_path, dst_archive)
343 if not ctx.Exists(gs_artifact_path):
344 cros_build_lib.Info('%s does not exist. Nothing to sign.',
345 gs_artifact_path)
346 continue
347
348 input_insn_path = input_insns.GetInsnFile(image_type)
349 if not os.path.exists(input_insn_path):
350 cros_build_lib.Info('%s does not exist. Nothing to sign.',
351 input_insn_path)
352 continue
353
354 # Generate the insn file for this artifact that the signer will use,
355 # and flag it for signing.
356 with tempfile.NamedTemporaryFile(
357 bufsize=0, prefix='pushimage.insns.') as insns_path:
358 input_insns.OutputInsns(image_type, insns_path.name, sect_insns,
359 sect_general)
360
361 gs_insns_path = '%s/%s' % (dst_path, dst_name)
362 if keyset != keysets[0]:
363 gs_insns_path += '-%s' % keyset
364 gs_insns_path += '.instructions'
365
366 ctx.Copy(insns_path.name, gs_insns_path)
Mike Frysingerdad40d62014-02-09 02:18:02 -0500367 MarkImageToBeSigned(ctx, tbs_base, gs_insns_path, priority)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400368 cros_build_lib.Info('Signing %s image %s', image_type, gs_insns_path)
Don Garrett9459c2f2014-01-22 18:20:24 -0800369 instruction_urls.setdefault(channel, []).append(gs_insns_path)
370
371 return instruction_urls
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400372
373
374def main(argv):
375 parser = commandline.ArgumentParser(description=__doc__)
376
377 # The type of image_dir will strip off trailing slashes (makes later
378 # processing simpler and the display prettier).
379 parser.add_argument('image_dir', default=None, type='local_or_gs_path',
380 help='full path of source artifacts to upload')
381 parser.add_argument('--board', default=None, required=True,
382 help='board to generate symbols for')
383 parser.add_argument('--profile', default=None,
384 help='board profile in use (e.g. "asan")')
385 parser.add_argument('--version', default=None,
386 help='version info (normally extracted from image_dir)')
387 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
388 help='show what would be done, but do not upload')
389 parser.add_argument('-M', '--mock', default=False, action='store_true',
390 help='upload things to a testing bucket (dev testing)')
Mike Frysingerdad40d62014-02-09 02:18:02 -0500391 parser.add_argument('--test-sign-mp', default=False, action='store_true',
392 help='mung signing behavior to sign w/test mp keys')
393 parser.add_argument('--test-sign-premp', default=False, action='store_true',
394 help='mung signing behavior to sign w/test premp keys')
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400395 parser.add_argument('--priority', type=int, default=50,
396 help='set signing priority (lower == higher prio)')
397 parser.add_argument('--sign-types', default=None, nargs='+',
398 choices=('recovery', 'factory', 'firmware'),
399 help='only sign specified image types')
400
401 opts = parser.parse_args(argv)
402 opts.Freeze()
403
Mike Frysingerdad40d62014-02-09 02:18:02 -0500404 force_keysets = set()
405 if opts.test_sign_mp:
406 force_keysets.add('test-keys-mp')
407 if opts.test_sign_premp:
408 force_keysets.add('test-keys-premp')
409
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400410 PushImage(opts.image_dir, opts.board, versionrev=opts.version,
411 profile=opts.profile, priority=opts.priority,
Mike Frysingerdad40d62014-02-09 02:18:02 -0500412 sign_types=opts.sign_types, dry_run=opts.dry_run, mock=opts.mock,
413 force_keysets=force_keysets)