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