blob: c8b2e9221c592a76fe159057c9b2f55ddcf09e39 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Mike Frysingerd13faeb2013-09-05 16:00:46 -04002# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Unittests for pushimage.py"""
7
Mike Frysinger6791b1d2014-03-04 20:52:22 -05008from __future__ import print_function
9
Mike Frysingerd7c93092019-10-14 00:12:50 -040010import collections
Mike Frysingerd13faeb2013-09-05 16:00:46 -040011import os
Mike Frysinger99a7f8a2020-04-19 06:23:18 -040012import sys
Mike Frysingerd13faeb2013-09-05 16:00:46 -040013
Mike Frysinger6db648e2018-07-24 19:57:58 -040014import mock
15
Mike Frysinger6791b1d2014-03-04 20:52:22 -050016from chromite.lib import cros_build_lib
Mike Frysingerd13faeb2013-09-05 16:00:46 -040017from chromite.lib import cros_test_lib
Don Garrett9fd20a82014-09-04 11:37:22 -070018from chromite.lib import gs
Mike Frysingerd13faeb2013-09-05 16:00:46 -040019from chromite.lib import gs_unittest
20from chromite.lib import osutils
Mike Frysinger4495b032014-03-05 17:24:03 -050021from chromite.lib import partial_mock
Mike Frysingerd13faeb2013-09-05 16:00:46 -040022from chromite.lib import signing
23from chromite.scripts import pushimage
24
25
Mike Frysinger99a7f8a2020-04-19 06:23:18 -040026assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
27
28
Greg Edelstona4c9b3b2020-01-07 17:51:13 -070029# Use our local copy of insns for testing as the main one is not available in
30# the public manifest. Even though _REL is a relative path, this works because
31# os.join leaves absolute paths on the right hand side alone.
32signing.INPUT_INSN_DIR_REL = signing.TEST_INPUT_INSN_DIR
33
34
Mike Frysingerd13faeb2013-09-05 16:00:46 -040035class InputInsnsTest(cros_test_lib.MockTestCase):
36 """Tests for InputInsns"""
37
Don Garrettac529772015-04-29 15:09:26 -070038 def setUp(self):
39 self.StartPatcher(gs_unittest.GSContextMock())
40
Mike Frysingerd13faeb2013-09-05 16:00:46 -040041 def testBasic(self):
42 """Simple smoke test"""
Don Garrettac529772015-04-29 15:09:26 -070043 insns = pushimage.InputInsns('test.board')
44 insns.GetInsnFile('recovery')
45 self.assertEqual(insns.GetChannels(), ['dev', 'canary'])
46 self.assertEqual(insns.GetKeysets(), ['stumpy-mp-v3'])
Mike Frysingerd13faeb2013-09-05 16:00:46 -040047
48 def testGetInsnFile(self):
49 """Verify various inputs result in right insns path"""
50 testdata = (
51 ('UPPER_CAPS', 'UPPER_CAPS'),
52 ('recovery', 'test.board'),
53 ('firmware', 'test.board.firmware'),
54 ('factory', 'test.board.factory'),
55 )
56 insns = pushimage.InputInsns('test.board')
57 for image_type, filename in testdata:
58 ret = insns.GetInsnFile(image_type)
59 self.assertEqual(os.path.basename(ret), '%s.instructions' % (filename))
60
61 def testSplitCfgField(self):
62 """Verify splitting behavior behaves"""
63 testdata = (
64 ('', []),
65 ('a b c', ['a', 'b', 'c']),
66 ('a, b', ['a', 'b']),
67 ('a,b', ['a', 'b']),
68 ('a,\tb', ['a', 'b']),
69 ('a\tb', ['a', 'b']),
70 )
71 for val, exp in testdata:
72 ret = pushimage.InputInsns.SplitCfgField(val)
73 self.assertEqual(ret, exp)
74
75 def testOutputInsnsBasic(self):
76 """Verify output instructions are sane"""
77 exp_content = """[insns]
Mike Frysingerd13faeb2013-09-05 16:00:46 -040078channel = dev canary
Mike Frysingerd84d91e2015-11-05 18:02:24 -050079keyset = stumpy-mp-v3
Mike Frysingerd13faeb2013-09-05 16:00:46 -040080chromeos_shell = false
81ensure_no_password = true
82firmware_update = true
83security_checks = true
84create_nplusone = true
85
86[general]
87"""
88
89 insns = pushimage.InputInsns('test.board')
Mike Frysinger37ccc2b2015-11-11 17:16:51 -050090 self.assertEqual(insns.GetAltInsnSets(), [None])
Mike Frysingerd13faeb2013-09-05 16:00:46 -040091 m = self.PatchObject(osutils, 'WriteFile')
Mike Frysingerd84d91e2015-11-05 18:02:24 -050092 insns.OutputInsns('/bogus', {}, {})
Mike Frysingerd13faeb2013-09-05 16:00:46 -040093 self.assertTrue(m.called)
94 content = m.call_args_list[0][0][1]
95 self.assertEqual(content.rstrip(), exp_content.rstrip())
96
97 def testOutputInsnsReplacements(self):
98 """Verify output instructions can be updated"""
99 exp_content = """[insns]
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400100channel = dev
Mike Frysingerd84d91e2015-11-05 18:02:24 -0500101keyset = batman
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400102chromeos_shell = false
103ensure_no_password = true
104firmware_update = true
105security_checks = true
106create_nplusone = true
107
108[general]
109board = board
110config_board = test.board
111"""
112 sect_insns = {
113 'channel': 'dev',
114 'keyset': 'batman',
115 }
Mike Frysingerd7c93092019-10-14 00:12:50 -0400116 sect_general = collections.OrderedDict((
117 ('board', 'board'),
118 ('config_board', 'test.board'),
119 ))
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400120
121 insns = pushimage.InputInsns('test.board')
122 m = self.PatchObject(osutils, 'WriteFile')
Mike Frysingerd84d91e2015-11-05 18:02:24 -0500123 insns.OutputInsns('/a/file', sect_insns, sect_general)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400124 self.assertTrue(m.called)
125 content = m.call_args_list[0][0][1]
126 self.assertEqual(content.rstrip(), exp_content.rstrip())
127
Mike Frysinger37ccc2b2015-11-11 17:16:51 -0500128 def testOutputInsnsMergeAlts(self):
129 """Verify handling of alternative insns.xxx sections"""
130 TEMPLATE_CONTENT = """[insns]
131channel = %(channel)s
132chromeos_shell = false
133ensure_no_password = true
134firmware_update = true
135security_checks = true
136create_nplusone = true
137override = sect_insns
138keyset = %(keyset)s
139%(extra)s
140[general]
141board = board
142config_board = test.board
143"""
144
145 exp_alts = ['insns.one', 'insns.two', 'insns.hotsoup']
146 exp_fields = {
147 'one': {'channel': 'dev canary', 'keyset': 'OneKeyset', 'extra': ''},
148 'two': {'channel': 'best', 'keyset': 'TwoKeyset', 'extra': ''},
149 'hotsoup': {
150 'channel': 'dev canary',
151 'keyset': 'ColdKeyset',
152 'extra': 'soup = cheddar\n',
153 },
154 }
155
156 # Make sure this overrides the insn sections.
157 sect_insns = {
158 'override': 'sect_insns',
159 }
160 sect_insns_copy = sect_insns.copy()
Mike Frysingerd7c93092019-10-14 00:12:50 -0400161 sect_general = collections.OrderedDict((
162 ('board', 'board'),
163 ('config_board', 'test.board'),
164 ))
Mike Frysinger37ccc2b2015-11-11 17:16:51 -0500165
166 insns = pushimage.InputInsns('test.multi')
167 self.assertEqual(insns.GetAltInsnSets(), exp_alts)
168 m = self.PatchObject(osutils, 'WriteFile')
169
170 for alt in exp_alts:
171 m.reset_mock()
172 insns.OutputInsns('/a/file', sect_insns, sect_general, insns_merge=alt)
173 self.assertEqual(sect_insns, sect_insns_copy)
174 self.assertTrue(m.called)
175 content = m.call_args_list[0][0][1]
176 exp_content = TEMPLATE_CONTENT % exp_fields[alt[6:]]
177 self.assertEqual(content.rstrip(), exp_content.rstrip())
178
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400179
Mike Frysinger4495b032014-03-05 17:24:03 -0500180class MarkImageToBeSignedTest(gs_unittest.AbstractGSContextTest):
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400181 """Tests for MarkImageToBeSigned()"""
182
183 def setUp(self):
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400184 # Minor optimization -- we call this for logging purposes in the main
185 # code, but don't really care about it for testing. It just slows us.
Mike Frysinger6430d132014-10-27 23:43:30 -0400186 self.PatchObject(cros_build_lib, 'MachineDetails', return_value='1234\n')
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400187
188 def testBasic(self):
189 """Simple smoke test"""
190 tbs_base = 'gs://some-bucket'
191 insns_path = 'chan/board/ver/file.instructions'
192 tbs_file = '%s/tobesigned/90,chan,board,ver,file.instructions' % tbs_base
193 ret = pushimage.MarkImageToBeSigned(self.ctx, tbs_base, insns_path, 90)
194 self.assertEqual(ret, tbs_file)
195
196 def testPriority(self):
197 """Verify diff priority values get used correctly"""
198 for prio, sprio in ((0, '00'), (9, '09'), (35, '35'), (99, '99')):
199 ret = pushimage.MarkImageToBeSigned(self.ctx, '', '', prio)
Mike Frysinger2d589a12019-08-25 14:15:12 -0400200 self.assertEqual(ret, '/tobesigned/%s,' % sprio)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400201
202 def testBadPriority(self):
203 """Verify we reject bad priority values"""
204 for prio in (-10, -1, 100, 91239):
205 self.assertRaises(ValueError, pushimage.MarkImageToBeSigned, self.ctx,
206 '', '', prio)
207
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400208 def testTbsUpload(self):
209 """Make sure we actually try to upload the file"""
210 pushimage.MarkImageToBeSigned(self.ctx, '', '', 50)
211 self.gs_mock.assertCommandContains(['cp', '--'])
212
213
Mike Frysinger4495b032014-03-05 17:24:03 -0500214class PushImageTests(gs_unittest.AbstractGSContextTest):
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400215 """Tests for PushImage()"""
216
217 def setUp(self):
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400218 self.mark_mock = self.PatchObject(pushimage, 'MarkImageToBeSigned')
219
220 def testBasic(self):
221 """Simple smoke test"""
Don Garrett9459c2f2014-01-22 18:20:24 -0800222 EXPECTED = {
223 'canary': [
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500224 ('gs://chromeos-releases/canary-channel/test.board-hi/5126.0.0/'
225 'ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions')],
Don Garrett9459c2f2014-01-22 18:20:24 -0800226 'dev': [
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500227 ('gs://chromeos-releases/dev-channel/test.board-hi/5126.0.0/'
228 'ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions')],
Don Garrett9459c2f2014-01-22 18:20:24 -0800229 }
Don Garrett9fd20a82014-09-04 11:37:22 -0700230 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
231 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
232 profile='hi')
Don Garrett9459c2f2014-01-22 18:20:24 -0800233
234 self.assertEqual(urls, EXPECTED)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400235
Amey Deshpande3c487952015-09-03 12:11:13 -0700236 def testBasic_SignTypesEmptyList(self):
237 """Tests PushImage behavior when |sign_types| is empty instead of None.
238
239 As part of the buildbots, PushImage function always receives a tuple for
240 |sign_types| argument. This test checks the behavior for empty tuple.
241 """
242 EXPECTED = {
243 'canary': [
244 ('gs://chromeos-releases/canary-channel/test.board-hi/5126.0.0/'
245 'ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions')],
246 'dev': [
247 ('gs://chromeos-releases/dev-channel/test.board-hi/5126.0.0/'
248 'ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions')],
249 }
250 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
251 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
252 profile='hi', sign_types=())
253
254 self.assertEqual(urls, EXPECTED)
255
Amey Deshpandea936c622015-08-12 17:27:54 -0700256 def testBasic_RealBoardName(self):
257 """Runs a simple smoke test using a real board name."""
258 EXPECTED = {
259 'canary': [
260 ('gs://chromeos-releases/canary-channel/x86-alex/5126.0.0/'
261 'ChromeOS-recovery-R34-5126.0.0-x86-alex.instructions')],
262 'dev': [
263 ('gs://chromeos-releases/dev-channel/x86-alex/5126.0.0/'
264 'ChromeOS-recovery-R34-5126.0.0-x86-alex.instructions')],
265 }
266 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
267 urls = pushimage.PushImage('/src', 'x86-alex', 'R34-5126.0.0')
268
269 self.assertEqual(urls, EXPECTED)
270
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400271 def testBasicMock(self):
272 """Simple smoke test in mock mode"""
Don Garrett9fd20a82014-09-04 11:37:22 -0700273 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
274 pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
275 dry_run=True, mock=True)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400276
277 def testBadVersion(self):
278 """Make sure we barf on bad version strings"""
279 self.assertRaises(ValueError, pushimage.PushImage, '', '', 'asdf')
280
281 def testNoInsns(self):
282 """Boards w/out insn files should get skipped"""
Don Garrett9459c2f2014-01-22 18:20:24 -0800283 urls = pushimage.PushImage('/src', 'a bad bad board', 'R34-5126.0.0')
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400284 self.assertEqual(self.gs_mock.call_count, 0)
Don Garrett9459c2f2014-01-22 18:20:24 -0800285 self.assertEqual(urls, None)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400286
287 def testSignTypesRecovery(self):
288 """Only sign the requested recovery type"""
Don Garrett9459c2f2014-01-22 18:20:24 -0800289 EXPECTED = {
290 'canary': [
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500291 ('gs://chromeos-releases/canary-channel/test.board/5126.0.0/'
292 'ChromeOS-recovery-R34-5126.0.0-test.board.instructions')],
Don Garrett9459c2f2014-01-22 18:20:24 -0800293 'dev': [
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500294 ('gs://chromeos-releases/dev-channel/test.board/5126.0.0/'
295 'ChromeOS-recovery-R34-5126.0.0-test.board.instructions')],
Don Garrett9459c2f2014-01-22 18:20:24 -0800296 }
297
Amey Deshpandea936c622015-08-12 17:27:54 -0700298 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
299 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
300 sign_types=['recovery'])
Mike Frysingerba0609c2020-07-21 05:19:06 -0400301 self.assertEqual(self.gs_mock.call_count, 30)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400302 self.assertTrue(self.mark_mock.called)
Don Garrett9459c2f2014-01-22 18:20:24 -0800303 self.assertEqual(urls, EXPECTED)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400304
Amey Deshpandea936c622015-08-12 17:27:54 -0700305 def testSignTypesBase(self):
306 """Only sign the requested recovery type"""
307 EXPECTED = {
308 'canary': [
309 ('gs://chromeos-releases/canary-channel/test.board/5126.0.0/'
310 'ChromeOS-base-R34-5126.0.0-test.board.instructions')],
311 'dev': [
312 ('gs://chromeos-releases/dev-channel/test.board/5126.0.0/'
313 'ChromeOS-base-R34-5126.0.0-test.board.instructions')],
314 }
315
316 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
317 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
318 sign_types=['base'])
Mike Frysingerba0609c2020-07-21 05:19:06 -0400319 self.assertEqual(self.gs_mock.call_count, 32)
LaMont Jones7d6c98f2019-09-27 12:37:33 -0600320 self.assertTrue(self.mark_mock.called)
321 self.assertEqual(urls, EXPECTED)
322
323 def testSignTypesCr50Firmware(self):
324 """Only sign the requested type"""
325 EXPECTED = {
326 'canary': [
327 ('gs://chromeos-releases/canary-channel/board2/5126.0.0/'
328 'ChromeOS-cr50_firmware-R34-5126.0.0-board2.instructions')],
329 'dev': [
330 ('gs://chromeos-releases/dev-channel/board2/5126.0.0/'
331 'ChromeOS-cr50_firmware-R34-5126.0.0-board2.instructions')],
332 }
333
334 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
335 urls = pushimage.PushImage('/src', 'board2', 'R34-5126.0.0',
336 sign_types=['cr50_firmware'])
Mike Frysingerba0609c2020-07-21 05:19:06 -0400337 self.assertEqual(self.gs_mock.call_count, 30)
Amey Deshpandea936c622015-08-12 17:27:54 -0700338 self.assertTrue(self.mark_mock.called)
339 self.assertEqual(urls, EXPECTED)
340
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400341 def testSignTypesNone(self):
342 """Verify nothing is signed when we request an unavailable type"""
Don Garrett9459c2f2014-01-22 18:20:24 -0800343 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
344 sign_types=['nononononono'])
Mike Frysingerba0609c2020-07-21 05:19:06 -0400345 self.assertEqual(self.gs_mock.call_count, 28)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400346 self.assertFalse(self.mark_mock.called)
Don Garrett9459c2f2014-01-22 18:20:24 -0800347 self.assertEqual(urls, {})
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400348
Mike Frysinger4495b032014-03-05 17:24:03 -0500349 def testGsError(self):
350 """Verify random GS errors don't make us blow up entirely"""
351 self.gs_mock.AddCmdResult(partial_mock.In('stat'), returncode=1,
352 output='gobblety gook\n')
353 with cros_test_lib.LoggingCapturer('chromite'):
354 self.assertRaises(pushimage.PushError, pushimage.PushImage, '/src',
355 'test.board', 'R34-5126.0.0')
356
Mike Frysingerd84d91e2015-11-05 18:02:24 -0500357 def testMultipleKeysets(self):
358 """Verify behavior when processing an insn w/multiple keysets"""
359 EXPECTED = {
360 'canary': [
361 ('gs://chromeos-releases/canary-channel/test.board/5126.0.0/'
362 'ChromeOS-recovery-R34-5126.0.0-test.board.instructions'),
363 ('gs://chromeos-releases/canary-channel/test.board/5126.0.0/'
364 'ChromeOS-recovery-R34-5126.0.0-test.board-key2.instructions'),
365 ('gs://chromeos-releases/canary-channel/test.board/5126.0.0/'
366 'ChromeOS-recovery-R34-5126.0.0-test.board-key3.instructions'),
367 ],
368 'dev': [
369 ('gs://chromeos-releases/dev-channel/test.board/5126.0.0/'
370 'ChromeOS-recovery-R34-5126.0.0-test.board.instructions'),
371 ('gs://chromeos-releases/dev-channel/test.board/5126.0.0/'
372 'ChromeOS-recovery-R34-5126.0.0-test.board-key2.instructions'),
373 ('gs://chromeos-releases/dev-channel/test.board/5126.0.0/'
374 'ChromeOS-recovery-R34-5126.0.0-test.board-key3.instructions'),
375 ],
376 }
377 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
378 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
379 force_keysets=('key1', 'key2', 'key3'))
380 self.assertEqual(urls, EXPECTED)
381
Mike Frysinger77912102017-08-30 18:35:46 -0400382 def testForceChannel(self):
383 """Verify behavior when user has specified custom channel"""
384 EXPECTED = {
385 'meep': [
386 ('gs://chromeos-releases/meep-channel/test.board/5126.0.0/'
387 'ChromeOS-recovery-R34-5126.0.0-test.board.instructions'),
388 ],
389 }
390 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
391 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
392 force_channels=('meep',))
393 self.assertEqual(urls, EXPECTED)
394
Mike Frysinger37ccc2b2015-11-11 17:16:51 -0500395 def testMultipleAltInsns(self):
396 """Verify behavior when processing an insn w/multiple insn overlays"""
397 EXPECTED = {
398 'canary': [
399 ('gs://chromeos-releases/canary-channel/test.multi/1.0.0/'
400 'ChromeOS-recovery-R1-1.0.0-test.multi.instructions'),
401 ('gs://chromeos-releases/canary-channel/test.multi/1.0.0/'
402 'ChromeOS-recovery-R1-1.0.0-test.multi-TwoKeyset.instructions'),
403 ('gs://chromeos-releases/canary-channel/test.multi/1.0.0/'
404 'ChromeOS-recovery-R1-1.0.0-test.multi-ColdKeyset.instructions'),
405 ],
406 'dev': [
407 ('gs://chromeos-releases/dev-channel/test.multi/1.0.0/'
408 'ChromeOS-recovery-R1-1.0.0-test.multi.instructions'),
409 ('gs://chromeos-releases/dev-channel/test.multi/1.0.0/'
410 'ChromeOS-recovery-R1-1.0.0-test.multi-TwoKeyset.instructions'),
411 ('gs://chromeos-releases/dev-channel/test.multi/1.0.0/'
412 'ChromeOS-recovery-R1-1.0.0-test.multi-ColdKeyset.instructions'),
413 ],
414 }
415 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
416 urls = pushimage.PushImage('/src', 'test.multi', 'R1-1.0.0')
417 self.assertEqual(urls, EXPECTED)
418
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400419
420class MainTests(cros_test_lib.MockTestCase):
421 """Tests for main()"""
422
423 def setUp(self):
424 self.PatchObject(pushimage, 'PushImage')
425
426 def testBasic(self):
427 """Simple smoke test"""
Mike Frysinger09fe0122014-02-09 02:44:05 -0500428 pushimage.main(['--board', 'test.board', '/src', '--yes'])