blob: e5afc85910ebd61cc2f4c79338768bf375250956 [file] [log] [blame]
Amin Hassani92f6c4a2021-02-20 17:36:09 -08001# Copyright 2021 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"""Library containing functions to install an image on a Chromium OS device."""
6
Amin Hassanid4b3ff82021-02-20 23:05:14 -08007import abc
Amin Hassani92f6c4a2021-02-20 17:36:09 -08008import enum
Daichi Hironoc1a8fd32022-01-07 22:17:51 +09009from io import BytesIO
Chris McDonald14ac61d2021-07-21 11:49:56 -060010import logging
Amin Hassani92f6c4a2021-02-20 17:36:09 -080011import os
12import re
Amin Hassanid4b3ff82021-02-20 23:05:14 -080013import tempfile
14import threading
Amin Hassani55970562021-02-22 20:49:13 -080015import time
Daichi Hironoc1a8fd32022-01-07 22:17:51 +090016from typing import Dict, List, Tuple, Union
Amin Hassani92f6c4a2021-02-20 17:36:09 -080017
Amin Hassani55970562021-02-22 20:49:13 -080018from chromite.cli import command
Amin Hassanicf8f0042021-03-12 10:42:13 -080019from chromite.cli import flash
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000020from chromite.lib import cgpt
Amin Hassanid4b3ff82021-02-20 23:05:14 -080021from chromite.lib import constants
Amin Hassani92f6c4a2021-02-20 17:36:09 -080022from chromite.lib import cros_build_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080023from chromite.lib import gs
Amin Hassanid4b3ff82021-02-20 23:05:14 -080024from chromite.lib import image_lib
Amin Hassani55970562021-02-22 20:49:13 -080025from chromite.lib import operation
Amin Hassanid4b3ff82021-02-20 23:05:14 -080026from chromite.lib import osutils
Amin Hassani92f6c4a2021-02-20 17:36:09 -080027from chromite.lib import parallel
28from chromite.lib import remote_access
29from chromite.lib import retry_util
Amin Hassani74403082021-02-22 11:40:09 -080030from chromite.lib import stateful_updater
Amin Hassani75c5f942021-02-20 23:56:53 -080031from chromite.lib.paygen import partition_lib
Amin Hassani74403082021-02-22 11:40:09 -080032from chromite.lib.paygen import paygen_stateful_payload_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080033from chromite.lib.xbuddy import devserver_constants
34from chromite.lib.xbuddy import xbuddy
Alex Klein18ef1212021-10-14 12:49:02 -060035from chromite.utils import timer
Amin Hassani92f6c4a2021-02-20 17:36:09 -080036
37
Amin Hassani92f6c4a2021-02-20 17:36:09 -080038class Error(Exception):
39 """Thrown when there is a general Chromium OS-specific flash error."""
40
41
42class ImageType(enum.Enum):
43 """Type of the image that is used for flashing the device."""
44
45 # The full image on disk (e.g. chromiumos_test_image.bin).
46 FULL = 0
47 # The remote directory path
48 # (e.g gs://chromeos-image-archive/eve-release/R90-x.x.x)
49 REMOTE_DIRECTORY = 1
50
51
52class Partition(enum.Enum):
53 """An enum for partition types like kernel and rootfs."""
54 KERNEL = 0
55 ROOTFS = 1
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000056 MINIOS = 2
Amin Hassani92f6c4a2021-02-20 17:36:09 -080057
58
59class DeviceImager(object):
60 """A class to flash a Chromium OS device.
61
62 This utility uses parallelism as much as possible to achieve its goal as fast
63 as possible. For example, it uses parallel compressors, parallel transfers,
64 and simultaneous pipes.
65 """
66
67 # The parameters of the kernel and rootfs's two main partitions.
68 A = {Partition.KERNEL: 2, Partition.ROOTFS: 3}
69 B = {Partition.KERNEL: 4, Partition.ROOTFS: 5}
70
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000071 MINIOS_A = {Partition.MINIOS: 9}
72 MINIOS_B = {Partition.MINIOS: 10}
73
Amin Hassani92f6c4a2021-02-20 17:36:09 -080074 def __init__(self, device, image: str,
Amin Hassanicf8f0042021-03-12 10:42:13 -080075 board: str = None,
76 version: str = None,
Amin Hassani92f6c4a2021-02-20 17:36:09 -080077 no_rootfs_update: bool = False,
78 no_stateful_update: bool = False,
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000079 no_minios_update: bool = False,
Amin Hassani92f6c4a2021-02-20 17:36:09 -080080 no_reboot: bool = False,
81 disable_verification: bool = False,
82 clobber_stateful: bool = False,
Daichi Hirono28831b3b2022-04-07 12:41:11 +090083 clear_tpm_owner: bool = False,
84 delta: bool = False):
Amin Hassani92f6c4a2021-02-20 17:36:09 -080085 """Initialize DeviceImager for flashing a Chromium OS device.
86
87 Args:
88 device: The ChromiumOSDevice to be updated.
89 image: The target image path (can be xBuddy path).
Amin Hassanicf8f0042021-03-12 10:42:13 -080090 board: Board to use.
91 version: Image version to use.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080092 no_rootfs_update: Whether to do rootfs partition update.
93 no_stateful_update: Whether to do stateful partition update.
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000094 no_minios_update: Whether to do minios partition update.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080095 no_reboot: Whether to reboot device after update. The default is True.
96 disable_verification: Whether to disabling rootfs verification on the
97 device.
98 clobber_stateful: Whether to do a clean stateful partition.
99 clear_tpm_owner: If true, it will clear the TPM owner on reboot.
Daichi Hirono28831b3b2022-04-07 12:41:11 +0900100 delta: Whether to use delta compression when transferring image bytes.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800101 """
102
103 self._device = device
104 self._image = image
Amin Hassanicf8f0042021-03-12 10:42:13 -0800105 self._board = board
106 self._version = version
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800107 self._no_rootfs_update = no_rootfs_update
108 self._no_stateful_update = no_stateful_update
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000109 self._no_minios_update = no_minios_update
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800110 self._no_reboot = no_reboot
111 self._disable_verification = disable_verification
112 self._clobber_stateful = clobber_stateful
113 self._clear_tpm_owner = clear_tpm_owner
114
Amin Hassanib1993eb2021-04-28 12:00:11 -0700115 self._image_type = None
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800116 self._inactive_state = None
Daichi Hirono28831b3b2022-04-07 12:41:11 +0900117 self._delta = delta
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800118
119 def Run(self):
120 """Update the device with image of specific version."""
Amin Hassanib1993eb2021-04-28 12:00:11 -0700121 self._LocateImage()
122 logging.notice('Preparing to update the remote device %s with image %s',
123 self._device.hostname, self._image)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800124
125 try:
Amin Hassani55970562021-02-22 20:49:13 -0800126 if command.UseProgressBar():
127 op = DeviceImagerOperation()
128 op.Run(self._Run)
129 else:
130 self._Run()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800131 except Exception as e:
132 raise Error(f'DeviceImager Failed with error: {e}')
133
Amin Hassani55970562021-02-22 20:49:13 -0800134 # DeviceImagerOperation will look for this log.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800135 logging.info('DeviceImager completed.')
136
137 def _Run(self):
138 """Runs the various operations to install the image on device."""
Daichi Hirono28831b3b2022-04-07 12:41:11 +0900139 # TODO(b/228389041): Switch to delta compression if self._delta is True
140
Amin Hassanib1993eb2021-04-28 12:00:11 -0700141 self._InstallPartitions()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800142
143 if self._clear_tpm_owner:
144 self._device.ClearTpmOwner()
145
146 if not self._no_reboot:
147 self._Reboot()
148 self._VerifyBootExpectations()
149
150 if self._disable_verification:
151 self._device.DisableRootfsVerification()
152
Amin Hassanib1993eb2021-04-28 12:00:11 -0700153 def _LocateImage(self):
154 """Locates the path to the final image(s) that need to be installed.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800155
156 If the paths is local, the image should be the Chromium OS GPT image
157 (e.g. chromiumos_test_image.bin). If the path is remote, it should be the
158 remote directory where we can find the quick-provision and stateful update
159 files (e.g. gs://chromeos-image-archive/eve-release/R90-x.x.x).
160
161 NOTE: At this point there is no caching involved. Hence we always download
162 the partition payloads or extract them from the Chromium OS image.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800163 """
164 if os.path.isfile(self._image):
Amin Hassanib1993eb2021-04-28 12:00:11 -0700165 self._image_type = ImageType.FULL
166 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800167
168 # TODO(b/172212406): We could potentially also allow this by searching
169 # through the directory to see whether we have quick-provision and stateful
170 # payloads. This only makes sense when a user has their workstation at home
171 # and doesn't want to incur the bandwidth cost of downloading the same
172 # image multiple times. For that, they can simply download the GPT image
173 # image first and flash that instead.
174 if os.path.isdir(self._image):
175 raise ValueError(
176 f'{self._image}: input must be a disk image, not a directory.')
177
178 if gs.PathIsGs(self._image):
179 # TODO(b/172212406): Check whether it is a directory. If it wasn't a
180 # directory download the image into some temp location and use it instead.
Amin Hassanib1993eb2021-04-28 12:00:11 -0700181 self._image_type = ImageType.REMOTE_DIRECTORY
182 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800183
184 # Assuming it is an xBuddy path.
Amin Hassanicf8f0042021-03-12 10:42:13 -0800185 board = cros_build_lib.GetBoard(
186 device_board=self._device.board or flash.GetDefaultBoard(),
187 override_board=self._board, force=True)
188
Amin Hassania20d20d2021-04-28 10:18:18 -0700189 xb = xbuddy.XBuddy(board=board, version=self._version)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800190 build_id, local_file = xb.Translate([self._image])
191 if build_id is None:
192 raise Error(f'{self._image}: unable to find matching xBuddy path.')
193 logging.info('XBuddy path translated to build ID %s', build_id)
194
195 if local_file:
Amin Hassanib1993eb2021-04-28 12:00:11 -0700196 self._image = local_file
197 self._image_type = ImageType.FULL
198 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800199
Amin Hassanib1993eb2021-04-28 12:00:11 -0700200 self._image = f'{devserver_constants.GS_IMAGE_DIR}/{build_id}'
201 self._image_type = ImageType.REMOTE_DIRECTORY
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800202
203 def _SplitDevPath(self, path: str) -> Tuple[str, int]:
204 """Splits the given /dev/x path into prefix and the dev number.
205
206 Args:
207 path: The path to a block dev device.
208
209 Returns:
210 A tuple of representing the prefix and the index of the dev path.
211 e.g.: '/dev/mmcblk0p1' -> ['/dev/mmcblk0p', 1]
212 """
213 match = re.search(r'(.*)([0-9]+)$', path)
214 if match is None:
215 raise Error(f'{path}: Could not parse root dev path.')
216
217 return match.group(1), int(match.group(2))
218
219 def _GetKernelState(self, root_num: int) -> Tuple[Dict, Dict]:
220 """Returns the kernel state.
221
222 Returns:
223 A tuple of two dictionaries: The current active kernel state and the
224 inactive kernel state. (Look at A and B constants in this class.)
225 """
226 if root_num == self.A[Partition.ROOTFS]:
227 return self.A, self.B
228 elif root_num == self.B[Partition.ROOTFS]:
229 return self.B, self.A
230 else:
231 raise Error(f'Invalid root partition number {root_num}')
232
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000233 def _GetMiniOSState(self, minios_num: int) -> Tuple[Dict, Dict]:
234 """Returns the miniOS state.
235
236 Returns:
237 A tuple of dictionaries: The current active miniOS state and the inactive
238 miniOS state.
239 """
240 if minios_num == self.MINIOS_A[Partition.MINIOS]:
241 return self.MINIOS_A, self.MINIOS_B
242 elif minios_num == self.MINIOS_B[Partition.MINIOS]:
243 return self.MINIOS_B, self.MINIOS_A
244 else:
245 raise Error(f'Invalid minios partition number {minios_num}')
246
Amin Hassanib1993eb2021-04-28 12:00:11 -0700247 def _InstallPartitions(self):
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800248 """The main method that installs the partitions of a Chrome OS device.
249
250 It uses parallelism to install the partitions as fast as possible.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800251 """
Amin Hassanid684e982021-02-26 11:10:58 -0800252 prefix, root_num = self._SplitDevPath(self._device.root_dev)
253 active_state, self._inactive_state = self._GetKernelState(root_num)
254
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800255 updaters = []
Amin Hassanid684e982021-02-26 11:10:58 -0800256 if not self._no_rootfs_update:
Amin Hassani75c5f942021-02-20 23:56:53 -0800257 current_root = prefix + str(active_state[Partition.ROOTFS])
258 target_root = prefix + str(self._inactive_state[Partition.ROOTFS])
Amin Hassanib1993eb2021-04-28 12:00:11 -0700259 updaters.append(RootfsUpdater(current_root, self._device, self._image,
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900260 self._image_type, target_root))
Amin Hassani75c5f942021-02-20 23:56:53 -0800261
Amin Hassanid684e982021-02-26 11:10:58 -0800262 target_kernel = prefix + str(self._inactive_state[Partition.KERNEL])
Amin Hassanib1993eb2021-04-28 12:00:11 -0700263 updaters.append(KernelUpdater(self._device, self._image, self._image_type,
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900264 target_kernel))
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800265
Amin Hassani74403082021-02-22 11:40:09 -0800266 if not self._no_stateful_update:
267 updaters.append(StatefulUpdater(self._clobber_stateful, self._device,
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900268 self._image, self._image_type, None))
Amin Hassani74403082021-02-22 11:40:09 -0800269
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000270 if not self._no_minios_update:
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700271 minios_priority = self._device.run(
272 ['crossystem', constants.MINIOS_PRIORITY]).stdout
273 if minios_priority not in ['A', 'B']:
274 logging.warning('Skipping miniOS flash due to missing priority.')
275 else:
276 # Reference disk_layout_v3 for partition numbering.
277 _, inactive_minios_state = self._GetMiniOSState(
278 9 if minios_priority == 'A' else 10)
279 target_minios = prefix + str(inactive_minios_state[Partition.MINIOS])
280 minios_updater = MiniOSUpdater(self._device, self._image,
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900281 self._image_type, target_minios)
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700282 updaters.append(minios_updater)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000283
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800284 # Retry the partitions updates that failed, in case a transient error (like
285 # SSH drop, etc) caused the error.
286 num_retries = 1
287 try:
288 retry_util.RetryException(Error, num_retries,
289 parallel.RunParallelSteps,
290 (x.Run for x in updaters if not x.IsFinished()),
291 halt_on_error=True)
292 except Exception:
293 # If one of the partitions failed to be installed, revert all partitions.
294 parallel.RunParallelSteps(x.Revert for x in updaters)
295 raise
296
297 def _Reboot(self):
298 """Reboots the device."""
299 try:
Amin Hassani9281b682021-03-08 16:38:25 -0800300 self._device.Reboot(timeout_sec=300)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800301 except remote_access.RebootError:
302 raise Error('Could not recover from reboot. Once example reason'
303 ' could be the image provided was a non-test image'
304 ' or the system failed to boot after the update.')
305 except Exception as e:
306 raise Error(f'Failed to reboot to the device with error: {e}')
307
308 def _VerifyBootExpectations(self):
309 """Verify that we fully booted into the expected kernel state."""
310 # Discover the newly active kernel.
311 _, root_num = self._SplitDevPath(self._device.root_dev)
312 active_state, _ = self._GetKernelState(root_num)
313
314 # If this happens, we should rollback.
315 if active_state != self._inactive_state:
316 raise Error('The expected kernel state after update is invalid.')
317
318 logging.info('Verified boot expectations.')
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800319
320
321class ReaderBase(threading.Thread):
322 """The base class for reading different inputs and writing into output.
323
324 This class extends threading.Thread, so it will be run on its own thread. Also
325 it can be used as a context manager. Internally, it opens necessary files for
326 writing to and reading from. This class cannot be instantiated, it needs to be
327 sub-classed first to provide necessary function implementations.
328 """
329
330 def __init__(self, use_named_pipes: bool = False):
331 """Initializes the class.
332
333 Args:
334 use_named_pipes: Whether to use a named pipe or anonymous file
335 descriptors.
336 """
337 super().__init__()
338 self._use_named_pipes = use_named_pipes
339 self._pipe_target = None
340 self._pipe_source = None
341
342 def __del__(self):
343 """Destructor.
344
345 Make sure to clean up any named pipes we might have created.
346 """
347 if self._use_named_pipes:
348 osutils.SafeUnlink(self._pipe_target)
349
350 def __enter__(self):
351 """Enters the context manager"""
352 if self._use_named_pipes:
353 # There is no need for the temp file, we only need its path. So the named
354 # pipe is created after this temp file is deleted.
355 with tempfile.NamedTemporaryFile(prefix='chromite-device-imager') as fp:
356 self._pipe_target = self._pipe_source = fp.name
357 os.mkfifo(self._pipe_target)
358 else:
359 self._pipe_target, self._pipe_source = os.pipe()
360
361 self.start()
362 return self
363
364 def __exit__(self, *args, **kwargs):
365 """Exits the context manager."""
366 self.join()
367
368 def _Source(self):
369 """Returns the source pipe to write data into.
370
371 Sub-classes can use this function to determine where to write their data
372 into.
373 """
374 return self._pipe_source
375
376 def _CloseSource(self):
377 """Closes the source pipe.
378
379 Sub-classes should use this function to close the pipe after they are done
380 writing into it. Failure to do so may result reader of the data to hang
381 indefinitely.
382 """
383 if not self._use_named_pipes:
384 os.close(self._pipe_source)
385
386 def Target(self):
387 """Returns the target pipe to read data from.
388
389 Users of this class can use this path to read data from.
390 """
391 return self._pipe_target
392
393 def CloseTarget(self):
394 """Closes the target pipe.
395
396 Users of this class should use this function to close the pipe after they
397 are done reading from it.
398 """
399 if self._use_named_pipes:
400 os.remove(self._pipe_target)
401 else:
402 os.close(self._pipe_target)
403
404
405class PartialFileReader(ReaderBase):
406 """A class to read specific offset and length from a file and compress it.
407
408 This class can be used to read from specific location and length in a file
409 (e.g. A partition in a GPT image). Then it compresses the input and writes it
410 out (to a pipe). Look at the base class for more information.
411 """
412
413 # The offset of different partitions in a Chromium OS image does not always
414 # align to larger values like 4096. It seems that 512 is the maximum value to
415 # be divisible by partition offsets. This size should not be increased just
416 # for 'performance reasons'. Since we are doing everything in parallel, in
417 # practice there is not much difference between this and larger block sizes as
418 # parallelism hides the possible extra latency provided by smaller block
419 # sizes.
420 _BLOCK_SIZE = 512
421
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900422 def __init__(self, image: str, offset: int, length: int,
423 compression_command: List[str]):
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800424 """Initializes the class.
425
426 Args:
427 image: The path to an image (local or remote directory).
428 offset: The offset (in bytes) to read from the image.
429 length: The length (in bytes) to read from the image.
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900430 compression_command: The command to compress transferred bytes.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800431 """
432 super().__init__()
433
434 self._image = image
435 self._offset = offset
436 self._length = length
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900437 self._compression_command = compression_command
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800438
439 def run(self):
440 """Runs the reading and compression."""
441 cmd = [
442 'dd',
443 'status=none',
444 f'if={self._image}',
445 f'ibs={self._BLOCK_SIZE}',
446 f'skip={int(self._offset/self._BLOCK_SIZE)}',
447 f'count={int(self._length/self._BLOCK_SIZE)}',
448 '|',
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900449 *self._compression_command,
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800450 ]
451
452 try:
453 cros_build_lib.run(' '.join(cmd), stdout=self._Source(), shell=True)
454 finally:
455 self._CloseSource()
456
457
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800458class GsFileCopier(ReaderBase):
459 """A class for downloading gzip compressed file from GS bucket into a pipe."""
460
461 def __init__(self, image: str):
462 """Initializes the class.
463
464 Args:
465 image: The path to an image (local or remote directory).
466 """
467 super().__init__(use_named_pipes=True)
468 self._image = image
469
470 def run(self):
471 """Runs the download and write into the output pipe."""
472 try:
473 gs.GSContext().Copy(self._image, self._Source())
474 finally:
475 self._CloseSource()
476
477
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800478class PartitionUpdaterBase(object):
479 """A base abstract class to use for installing an image into a partition.
480
481 Sub-classes should implement the abstract methods to provide the core
482 functionality.
483 """
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900484 def __init__(self, device, image: str, image_type, target: str):
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800485 """Initializes this base class with values that most sub-classes will need.
486
487 Args:
488 device: The ChromiumOSDevice to be updated.
489 image: The target image path for the partition update.
490 image_type: The type of the image (ImageType).
491 target: The target path (e.g. block dev) to install the update.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800492 """
493 self._device = device
494 self._image = image
495 self._image_type = image_type
496 self._target = target
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800497 self._finished = False
498
499 def Run(self):
500 """The main function that does the partition update job."""
Alex Klein18ef1212021-10-14 12:49:02 -0600501 with timer.Timer() as t:
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800502 try:
503 self._Run()
504 finally:
505 self._finished = True
506
Alex Klein18ef1212021-10-14 12:49:02 -0600507 logging.debug('Completed %s in %s', self.__class__.__name__, t)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800508
509 @abc.abstractmethod
510 def _Run(self):
511 """The method that need to be implemented by sub-classes."""
512 raise NotImplementedError('Sub-classes need to implement this.')
513
514 def IsFinished(self):
515 """Returns whether the partition update has been successful."""
516 return self._finished
517
518 @abc.abstractmethod
519 def Revert(self):
520 """Reverts the partition update.
521
522 Sub-classes need to implement this function to provide revert capability.
523 """
524 raise NotImplementedError('Sub-classes need to implement this.')
525
526
527class RawPartitionUpdater(PartitionUpdaterBase):
528 """A class to update a raw partition on a Chromium OS device."""
529
530 def _Run(self):
531 """The function that does the job of kernel partition update."""
532 if self._image_type == ImageType.FULL:
533 self._CopyPartitionFromImage(self._GetPartitionName())
534 elif self._image_type == ImageType.REMOTE_DIRECTORY:
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800535 self._RedirectPartition(self._GetRemotePartitionName())
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800536 else:
537 raise ValueError(f'Invalid image type {self._image_type}')
538
539 def _GetPartitionName(self):
540 """Returns the name of the partition in a Chromium OS GPT layout.
541
542 Subclasses should override this function to return correct name.
543 """
544 raise NotImplementedError('Subclasses need to implement this.')
545
546 def _CopyPartitionFromImage(self, part_name: str):
547 """Updates the device's partition from a local Chromium OS image.
548
549 Args:
550 part_name: The name of the partition in the source image that needs to be
551 extracted.
552 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800553 offset, length = self._GetPartLocation(part_name)
554 offset, length = self._OptimizePartLocation(offset, length)
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900555 compressor, decompressor = self._GetCompressionAndDecompression()
556
557 with PartialFileReader(self._image, offset, length, compressor) \
558 as generator:
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800559 try:
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900560 self._WriteToTarget(generator.Target(), decompressor)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800561 finally:
562 generator.CloseTarget()
563
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900564 def _GetCompressionAndDecompression(self) -> Tuple[List[str], List[str]]:
565 """Returns compression / decompression commands."""
566
567 return (
568 [cros_build_lib.FindCompressor(cros_build_lib.COMP_GZIP)],
569 self._device.GetDecompressor(cros_build_lib.COMP_GZIP),
570 )
571
572 def _WriteToTarget(self, source: Union[int, BytesIO],
573 decompress_command: List[str]) -> None:
574 """Writes bytes source to the target device on DUT.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800575
576 Returns:
577 A string command to run on a device to read data from stdin, uncompress it
578 and write it to the target partition.
579 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800580 # Using oflag=direct to tell the OS not to cache the writes (faster).
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900581 cmd = ' '.join([
582 *decompress_command,
583 '|', 'dd', 'bs=1M', 'oflag=direct', f'of={self._target}'])
584 self._device.run(cmd, input=source, shell=True)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800585
586 def _GetPartLocation(self, part_name: str):
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000587 """Extracts the location and size of the raw partition from the image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800588
589 Args:
590 part_name: The name of the partition in the source image that needs to be
591 extracted.
592
593 Returns:
594 A tuple of offset and length (in bytes) from the image.
595 """
596 try:
597 parts = image_lib.GetImageDiskPartitionInfo(self._image)
598 part_info = [p for p in parts if p.name == part_name][0]
599 except IndexError:
600 raise Error(f'No partition named {part_name} found.')
601
602 return int(part_info.start), int(part_info.size)
603
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800604 def _GetRemotePartitionName(self):
605 """Returns the name of the quick-provision partition file.
606
607 Subclasses should override this function to return correct name.
608 """
609 raise NotImplementedError('Subclasses need to implement this.')
610
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800611 def _OptimizePartLocation(self, offset: int, length: int):
612 """Optimizes the offset and length of the partition.
613
614 Subclasses can override this to provide better offset/length than what is
615 defined in the PGT partition layout.
616
617 Args:
618 offset: The offset (in bytes) of the partition in the image.
619 length: The length (in bytes) of the partition.
620
621 Returns:
622 A tuple of offset and length (in bytes) from the image.
623 """
624 return offset, length
Amin Hassanid684e982021-02-26 11:10:58 -0800625
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800626 def _RedirectPartition(self, file_name: str):
627 """Downloads the partition from a remote path and writes it into target.
628
629 Args:
630 file_name: The file name in the remote directory self._image.
631 """
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800632 image_path = os.path.join(self._image, file_name)
633 with GsFileCopier(image_path) as generator:
634 try:
635 with open(generator.Target(), 'rb') as fp:
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900636 # Always use GZIP as remote quick provision images are gzip
637 # compressed only.
638 self._WriteToTarget(
639 fp, self._device.GetDecompressor(cros_build_lib.COMP_GZIP))
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800640 finally:
641 generator.CloseTarget()
642
Amin Hassanid684e982021-02-26 11:10:58 -0800643
644class KernelUpdater(RawPartitionUpdater):
645 """A class to update the kernel partition on a Chromium OS device."""
646
647 def _GetPartitionName(self):
648 """See RawPartitionUpdater._GetPartitionName()."""
649 return constants.PART_KERN_B
650
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800651 def _GetRemotePartitionName(self):
652 """See RawPartitionUpdater._GetRemotePartitionName()."""
653 return constants.QUICK_PROVISION_PAYLOAD_KERNEL
654
Amin Hassanid684e982021-02-26 11:10:58 -0800655 def Revert(self):
656 """Reverts the kernel partition update."""
657 # There is nothing to do for reverting kernel partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800658
659
660class RootfsUpdater(RawPartitionUpdater):
661 """A class to update the root partition on a Chromium OS device."""
662
663 def __init__(self, current_root: str, *args):
664 """Initializes the class.
665
666 Args:
667 current_root: The current root device path.
668 *args: See PartitionUpdaterBase
669 """
670 super().__init__(*args)
671
672 self._current_root = current_root
673 self._ran_postinst = False
674
675 def _GetPartitionName(self):
676 """See RawPartitionUpdater._GetPartitionName()."""
677 return constants.PART_ROOT_A
678
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800679 def _GetRemotePartitionName(self):
680 """See RawPartitionUpdater._GetRemotePartitionName()."""
681 return constants.QUICK_PROVISION_PAYLOAD_ROOTFS
682
Amin Hassani75c5f942021-02-20 23:56:53 -0800683 def _Run(self):
684 """The function that does the job of rootfs partition update."""
Amin Hassani55970562021-02-22 20:49:13 -0800685 with ProgressWatcher(self._device, self._target):
686 super()._Run()
Amin Hassani75c5f942021-02-20 23:56:53 -0800687
688 self._RunPostInst()
689
690 def _OptimizePartLocation(self, offset: int, length: int):
691 """Optimizes the size of the root partition of the image.
692
693 Normally the file system does not occupy the entire partition. Furthermore
694 we don't need the verity hash tree at the end of the root file system
695 because postinst will recreate it. This function reads the (approximate)
696 superblock of the ext4 partition and extracts the actual file system size in
697 the root partition.
698 """
699 superblock_size = 4096 * 2
700 with open(self._image, 'rb') as r:
701 r.seek(offset)
702 with tempfile.NamedTemporaryFile(delete=False) as fp:
703 fp.write(r.read(superblock_size))
704 fp.close()
705 return offset, partition_lib.Ext2FileSystemSize(fp.name)
706
707 def _RunPostInst(self, on_target: bool = True):
708 """Runs the postinst process in the root partition.
709
710 Args:
711 on_target: If true the postinst is run on the target (inactive)
712 partition. This is used when doing normal updates. If false, the
713 postinst is run on the current (active) partition. This is used when
714 reverting an update.
715 """
716 try:
717 postinst_dir = '/'
718 partition = self._current_root
719 if on_target:
720 postinst_dir = self._device.run(
721 ['mktemp', '-d', '-p', self._device.work_dir],
722 capture_output=True).stdout.strip()
723 self._device.run(['mount', '-o', 'ro', self._target, postinst_dir])
724 partition = self._target
725
726 self._ran_postinst = True
727 postinst = os.path.join(postinst_dir, 'postinst')
728 result = self._device.run([postinst, partition], capture_output=True)
729
730 logging.debug('Postinst result on %s: \n%s', postinst, result.stdout)
Amin Hassani55970562021-02-22 20:49:13 -0800731 # DeviceImagerOperation will look for this log.
Amin Hassani75c5f942021-02-20 23:56:53 -0800732 logging.info('Postinstall completed.')
733 finally:
734 if on_target:
735 self._device.run(['umount', postinst_dir])
736
737 def Revert(self):
738 """Reverts the root update install."""
739 logging.info('Reverting the rootfs partition update.')
740 if self._ran_postinst:
741 # We don't have to do anything for revert if we haven't changed the kernel
742 # priorities yet.
743 self._RunPostInst(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800744
745
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000746class MiniOSUpdater(RawPartitionUpdater):
747 """A class to update the miniOS partition on a Chromium OS device."""
748
749 def __init__(self, *args):
750 """Initializes the class.
751
752 Args:
753 *args: See PartitionUpdaterBase
754 """
755 super().__init__(*args)
756
757 self._ran_postinst = False
758
759 def _GetPartitionName(self):
760 """See RawPartitionUpdater._GetPartitionName()."""
761 return constants.PART_MINIOS_A
762
763 def _GetRemotePartitionName(self):
764 """See RawPartitionUpdater._GetRemotePartitionName()."""
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700765 return constants.QUICK_PROVISION_PAYLOAD_MINIOS
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000766
767 def _Run(self):
768 """The function that does the job of rootfs partition update."""
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700769 if self._image_type == ImageType.FULL:
770 if self._MiniOSPartitionsExistInImage():
771 logging.info('Updating miniOS partition from local.')
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000772 super()._Run()
773 else:
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700774 logging.warning('Not updating miniOS partition as it does not exist.')
775 return
776 elif self._image_type == ImageType.REMOTE_DIRECTORY:
777 if not gs.GSContext().Exists(
778 os.path.join(self._image,
779 constants.QUICK_PROVISION_PAYLOAD_MINIOS)):
780 logging.warning('Not updating miniOS, missing remote files.')
781 return
782 elif not self._MiniOSPartitionsExist():
783 logging.warning('Not updating miniOS, missing partitions.')
784 return
785 else:
786 logging.info('Updating miniOS partition from remote.')
787 super()._Run()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000788 else:
789 # Let super() handle this error.
790 super()._Run()
791
792 self._RunPostInstall()
793
794 def _RunPostInstall(self):
795 """The function will change the priority of the miniOS partitions."""
796 self._FlipMiniOSPriority()
797 self._ran_postinst = True
798
799 def Revert(self):
800 """Reverts the miniOS partition update."""
801 if self._ran_postinst:
802 self._FlipMiniOSPriority()
803
804 def _GetMiniOSPriority(self):
Mike Frysinger7a580c12022-08-01 21:45:41 -0400805 return self._device.run(['crossystem', constants.MINIOS_PRIORITY]).stdout
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000806
807 def _SetMiniOSPriority(self, priority: str):
808 self._device.run(
809 ['crossystem', f'{constants.MINIOS_PRIORITY}={priority}'])
810
811 def _FlipMiniOSPriority(self):
812 inactive_minios_priority = 'B' if self._GetMiniOSPriority() == 'A' else 'A'
813 logging.info('Setting miniOS priority to %s', inactive_minios_priority)
814 self._SetMiniOSPriority(inactive_minios_priority)
815
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700816 def _MiniOSPartitionsExistInImage(self):
817 """Checks if miniOS partition exists in the image."""
818 d = cgpt.Disk.FromImage(self._image)
819 try:
820 d.GetPartitionByTypeGuid(cgpt.MINIOS_TYPE_GUID)
821 return True
822 except KeyError:
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000823 return False
824
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700825 def _MiniOSPartitionsExist(self):
826 """Checks if the device has miniOS partitions."""
827 run = lambda x: self._device.run(x).stdout.strip()
828 device_drive = run(['rootdev', '-s', '-d'])
829 cmd = ['cgpt', 'show', '-t', device_drive, '-i']
830 return all((run(cmd + [p]) == cgpt.MINIOS_TYPE_GUID) for p in ('9', '10'))
831
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000832
Amin Hassani74403082021-02-22 11:40:09 -0800833class StatefulPayloadGenerator(ReaderBase):
834 """A class for generating a stateful update payload in a separate thread."""
835 def __init__(self, image: str):
836 """Initializes that class.
837
838 Args:
839 image: The path to a local Chromium OS image.
840 """
841 super().__init__()
842 self._image = image
843
844 def run(self):
845 """Generates the stateful update and writes it into the output pipe."""
846 try:
847 paygen_stateful_payload_lib.GenerateStatefulPayload(
848 self._image, self._Source())
849 finally:
850 self._CloseSource()
851
852
853class StatefulUpdater(PartitionUpdaterBase):
854 """A class to update the stateful partition on a device."""
855 def __init__(self, clobber_stateful: bool, *args):
856 """Initializes the class
857
858 Args:
859 clobber_stateful: Whether to clobber the stateful or not.
860 *args: Look at PartitionUpdaterBase.
861 """
862 super().__init__(*args)
863 self._clobber_stateful = clobber_stateful
864
865 def _Run(self):
866 """Reads/Downloads the stateful updates and writes it into the device."""
867 if self._image_type == ImageType.FULL:
868 generator_cls = StatefulPayloadGenerator
869 elif self._image_type == ImageType.REMOTE_DIRECTORY:
870 generator_cls = GsFileCopier
871 self._image = os.path.join(self._image,
872 paygen_stateful_payload_lib.STATEFUL_FILE)
873 else:
874 raise ValueError(f'Invalid image type {self._image_type}')
875
876 with generator_cls(self._image) as generator:
877 try:
878 updater = stateful_updater.StatefulUpdater(self._device)
879 updater.Update(
880 generator.Target(),
881 is_payload_on_device=False,
882 update_type=(stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER if
883 self._clobber_stateful else None))
884 finally:
885 generator.CloseTarget()
886
887 def Revert(self):
888 """Reverts the stateful partition update."""
889 logging.info('Reverting the stateful update.')
890 stateful_updater.StatefulUpdater(self._device).Reset()
Amin Hassani55970562021-02-22 20:49:13 -0800891
892
893class ProgressWatcher(threading.Thread):
894 """A class used for watching the progress of rootfs update."""
895
896 def __init__(self, device, target_root: str):
897 """Initializes the class.
898
899 Args:
900 device: The ChromiumOSDevice to be updated.
901 target_root: The target root partition to monitor the progress of.
902 """
903 super().__init__()
904
905 self._device = device
906 self._target_root = target_root
907 self._exit = False
908
909 def __enter__(self):
910 """Starts the thread."""
911 self.start()
912 return self
913
914 def __exit__(self, *args, **kwargs):
915 """Exists the thread."""
916 self._exit = True
917 self.join()
918
919 def _ShouldExit(self):
920 return self._exit
921
922 def run(self):
923 """Monitors the progress of the target root partitions' update.
924
925 This is done by periodically, reading the fd position of the process that is
926 writing into the target partition and reporting it back. Then the position
927 is divided by the size of the block device to report an approximate
928 progress.
929 """
930 cmd = ['blockdev', '--getsize64', self._target_root]
931 output = self._device.run(cmd, capture_output=True).stdout.strip()
932 if output is None:
933 raise Error(f'Cannot get the block device size from {output}.')
934 dev_size = int(output)
935
936 # Using lsof to find out which process is writing to the target rootfs.
Benjamin Gordonfb2f1a72022-06-23 09:19:41 -0600937 cmd = ['lsof', '-t', self._target_root]
Amin Hassani55970562021-02-22 20:49:13 -0800938 while not self._ShouldExit():
939 try:
Benjamin Gordonfb2f1a72022-06-23 09:19:41 -0600940 pid = self._device.run(cmd, capture_output=True).stdout.strip()
941 if pid:
Amin Hassani55970562021-02-22 20:49:13 -0800942 break
943 except cros_build_lib.RunCommandError:
944 continue
945 finally:
946 time.sleep(1)
947
948 # Now that we know which process is writing to it, we can look the fdinfo of
949 # stdout of that process to get its offset. We're assuming there will be no
950 # seek, which is correct.
Amin Hassani55970562021-02-22 20:49:13 -0800951 cmd = ['cat', f'/proc/{pid}/fdinfo/1']
952 while not self._ShouldExit():
953 try:
954 output = self._device.run(cmd, capture_output=True).stdout.strip()
955 m = re.search(r'^pos:\s*(\d+)$', output, flags=re.M)
956 if m:
957 offset = int(m.group(1))
958 # DeviceImagerOperation will look for this log.
959 logging.info('RootFS progress: %f', offset/dev_size)
960 except cros_build_lib.RunCommandError:
961 continue
962 finally:
963 time.sleep(1)
964
965
966class DeviceImagerOperation(operation.ProgressBarOperation):
967 """A class to provide a progress bar for DeviceImager operation."""
968
969 def __init__(self):
970 """Initializes the class."""
971 super().__init__()
972
973 self._progress = 0.0
974
975 def ParseOutput(self, output=None):
976 """Override function to parse the output and provide progress.
977
978 Args:
979 output: The stderr or stdout.
980 """
981 output = self._stdout.read()
982 match = re.findall(r'RootFS progress: (\d+(?:\.\d+)?)', output)
983 if match:
984 progress = float(match[0])
985 self._progress = max(self._progress, progress)
986
987 # If postinstall completes, move half of the remaining progress.
988 if re.findall(r'Postinstall completed', output):
989 self._progress += (1.0 - self._progress) / 2
990
991 # While waiting for reboot, each time, move half of the remaining progress.
992 if re.findall(r'Unable to get new boot_id', output):
993 self._progress += (1.0 - self._progress) / 2
994
995 if re.findall(r'DeviceImager completed.', output):
996 self._progress = 1.0
997
998 self.ProgressBar(self._progress)