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