blob: 0a54715a49ccacdd6f8c54e5ff6644326b2264bb [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2021 The ChromiumOS Authors
Amin Hassani92f6c4a2021-02-20 17:36:09 -08002# 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):
Alex Klein1699fab2022-09-08 08:46:06 -060039 """Thrown when there is a general Chromium OS-specific flash error."""
Amin Hassani92f6c4a2021-02-20 17:36:09 -080040
41
42class ImageType(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060043 """Type of the image that is used for flashing the device."""
Amin Hassani92f6c4a2021-02-20 17:36:09 -080044
Alex Klein1699fab2022-09-08 08:46:06 -060045 # 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
Amin Hassani92f6c4a2021-02-20 17:36:09 -080050
51
52class Partition(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060053 """An enum for partition types like kernel and rootfs."""
54
55 KERNEL = 0
56 ROOTFS = 1
57 MINIOS = 2
Amin Hassani92f6c4a2021-02-20 17:36:09 -080058
59
60class DeviceImager(object):
Alex Klein1699fab2022-09-08 08:46:06 -060061 """A class to flash a Chromium OS device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080062
Alex Klein975e86c2023-01-23 16:49:10 -070063 This utility uses parallelism as much as possible to achieve its goal as
64 fast as possible. For example, it uses parallel compressors, parallel
65 transfers, and simultaneous pipes.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080066 """
67
Alex Klein1699fab2022-09-08 08:46:06 -060068 # The parameters of the kernel and rootfs's two main partitions.
69 A = {Partition.KERNEL: 2, Partition.ROOTFS: 3}
70 B = {Partition.KERNEL: 4, Partition.ROOTFS: 5}
Amin Hassani92f6c4a2021-02-20 17:36:09 -080071
Alex Klein1699fab2022-09-08 08:46:06 -060072 MINIOS_A = {Partition.MINIOS: 9}
73 MINIOS_B = {Partition.MINIOS: 10}
Amin Hassani92f6c4a2021-02-20 17:36:09 -080074
Alex Klein1699fab2022-09-08 08:46:06 -060075 def __init__(
76 self,
77 device,
78 image: str,
79 board: str = None,
80 version: str = None,
81 no_rootfs_update: bool = False,
82 no_stateful_update: bool = False,
83 no_minios_update: bool = False,
84 no_reboot: bool = False,
85 disable_verification: bool = False,
86 clobber_stateful: bool = False,
87 clear_tpm_owner: bool = False,
88 delta: bool = False,
89 ):
90 """Initialize DeviceImager for flashing a Chromium OS device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080091
Alex Klein1699fab2022-09-08 08:46:06 -060092 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -060093 device: The ChromiumOSDevice to be updated.
94 image: The target image path (can be xBuddy path).
95 board: Board to use.
96 version: Image version to use.
97 no_rootfs_update: Whether to do rootfs partition update.
98 no_stateful_update: Whether to do stateful partition update.
99 no_minios_update: Whether to do minios partition update.
100 no_reboot: Whether to reboot device after update, default True.
101 disable_verification: Whether to disable rootfs verification on the
102 device.
103 clobber_stateful: Whether to do a clean stateful partition.
104 clear_tpm_owner: If true, it will clear the TPM owner on reboot.
105 delta: Whether to use delta compression when transferring image
106 bytes.
Alex Klein1699fab2022-09-08 08:46:06 -0600107 """
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800108
Alex Klein1699fab2022-09-08 08:46:06 -0600109 self._device = device
110 self._image = image
111 self._board = board
112 self._version = version
113 self._no_rootfs_update = no_rootfs_update
114 self._no_stateful_update = no_stateful_update
115 self._no_minios_update = no_minios_update
116 self._no_reboot = no_reboot
117 self._disable_verification = disable_verification
118 self._clobber_stateful = clobber_stateful
119 self._clear_tpm_owner = clear_tpm_owner
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800120
Alex Klein1699fab2022-09-08 08:46:06 -0600121 self._image_type = None
122 self._inactive_state = None
123 self._delta = delta
Daichi Hirono28831b3b2022-04-07 12:41:11 +0900124
Alex Klein1699fab2022-09-08 08:46:06 -0600125 def Run(self):
126 """Update the device with image of specific version."""
127 self._LocateImage()
128 logging.notice(
129 "Preparing to update the remote device %s with image %s",
130 self._device.hostname,
131 self._image,
132 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800133
Alex Klein1699fab2022-09-08 08:46:06 -0600134 try:
135 if command.UseProgressBar():
136 op = DeviceImagerOperation()
137 op.Run(self._Run)
138 else:
139 self._Run()
140 except Exception as e:
141 raise Error(f"DeviceImager Failed with error: {e}")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800142
Alex Klein1699fab2022-09-08 08:46:06 -0600143 # DeviceImagerOperation will look for this log.
144 logging.info("DeviceImager completed.")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800145
Alex Klein1699fab2022-09-08 08:46:06 -0600146 def _Run(self):
147 """Runs the various operations to install the image on device."""
148 # TODO(b/228389041): Switch to delta compression if self._delta is True
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800149
Alex Klein1699fab2022-09-08 08:46:06 -0600150 self._InstallPartitions()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800151
Alex Klein1699fab2022-09-08 08:46:06 -0600152 if self._clear_tpm_owner:
153 self._device.ClearTpmOwner()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800154
Daichi Hirono1d45ed52023-01-20 17:30:26 +0900155 if self._disable_verification:
156 # DisableRootfsVerification internally invokes Reboot().
157 self._device.DisableRootfsVerification()
158 self._VerifyBootExpectations()
159 elif not self._no_reboot:
Alex Klein1699fab2022-09-08 08:46:06 -0600160 self._Reboot()
161 self._VerifyBootExpectations()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800162
Alex Klein1699fab2022-09-08 08:46:06 -0600163 def _LocateImage(self):
164 """Locates the path to the final image(s) that need to be installed.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800165
Alex Klein1699fab2022-09-08 08:46:06 -0600166 If the paths is local, the image should be the Chromium OS GPT image
Alex Klein975e86c2023-01-23 16:49:10 -0700167 (e.g. chromiumos_test_image.bin). If the path is remote, it should be
168 the remote directory where we can find the quick-provision and stateful
169 update files (e.g. gs://chromeos-image-archive/eve-release/R90-x.x.x).
Amin Hassanicf8f0042021-03-12 10:42:13 -0800170
Alex Klein975e86c2023-01-23 16:49:10 -0700171 NOTE: At this point there is no caching involved. Hence we always
172 download the partition payloads or extract them from the Chromium OS
173 image.
Alex Klein1699fab2022-09-08 08:46:06 -0600174 """
175 if os.path.isfile(self._image):
176 self._image_type = ImageType.FULL
177 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800178
Alex Klein1699fab2022-09-08 08:46:06 -0600179 # TODO(b/172212406): We could potentially also allow this by searching
Alex Klein975e86c2023-01-23 16:49:10 -0700180 # through the directory to see whether we have quick-provision and
181 # stateful payloads. This only makes sense when a user has their
182 # workstation at home and doesn't want to incur the bandwidth cost of
183 # downloading the same image multiple times. For that, they can simply
184 # download the GPT image image first and flash that instead.
Alex Klein1699fab2022-09-08 08:46:06 -0600185 if os.path.isdir(self._image):
186 raise ValueError(
187 f"{self._image}: input must be a disk image, not a directory."
188 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800189
Alex Klein1699fab2022-09-08 08:46:06 -0600190 if gs.PathIsGs(self._image):
191 # TODO(b/172212406): Check whether it is a directory. If it wasn't a
Alex Klein975e86c2023-01-23 16:49:10 -0700192 # directory download the image into some temp location and use it
193 # instead.
Alex Klein1699fab2022-09-08 08:46:06 -0600194 self._image_type = ImageType.REMOTE_DIRECTORY
195 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800196
Alex Klein1699fab2022-09-08 08:46:06 -0600197 # Assuming it is an xBuddy path.
198 board = cros_build_lib.GetBoard(
199 device_board=self._device.board or flash.GetDefaultBoard(),
200 override_board=self._board,
201 force=True,
202 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800203
Alex Klein1699fab2022-09-08 08:46:06 -0600204 xb = xbuddy.XBuddy(board=board, version=self._version)
205 build_id, local_file = xb.Translate([self._image])
206 if build_id is None:
207 raise Error(f"{self._image}: unable to find matching xBuddy path.")
208 logging.info("XBuddy path translated to build ID %s", build_id)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800209
Alex Klein1699fab2022-09-08 08:46:06 -0600210 if local_file:
211 self._image = local_file
212 self._image_type = ImageType.FULL
213 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800214
Alex Klein1699fab2022-09-08 08:46:06 -0600215 self._image = f"{devserver_constants.GS_IMAGE_DIR}/{build_id}"
216 self._image_type = ImageType.REMOTE_DIRECTORY
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800217
Alex Klein1699fab2022-09-08 08:46:06 -0600218 def _SplitDevPath(self, path: str) -> Tuple[str, int]:
219 """Splits the given /dev/x path into prefix and the dev number.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800220
Alex Klein1699fab2022-09-08 08:46:06 -0600221 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600222 path: The path to a block dev device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800223
Alex Klein1699fab2022-09-08 08:46:06 -0600224 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600225 A tuple of representing the prefix and the index of the dev path.
226 e.g.: '/dev/mmcblk0p1' -> ['/dev/mmcblk0p', 1]
Alex Klein1699fab2022-09-08 08:46:06 -0600227 """
228 match = re.search(r"(.*)([0-9]+)$", path)
229 if match is None:
230 raise Error(f"{path}: Could not parse root dev path.")
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000231
Alex Klein1699fab2022-09-08 08:46:06 -0600232 return match.group(1), int(match.group(2))
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000233
Alex Klein1699fab2022-09-08 08:46:06 -0600234 def _GetKernelState(self, root_num: int) -> Tuple[Dict, Dict]:
235 """Returns the kernel state.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800236
Alex Klein1699fab2022-09-08 08:46:06 -0600237 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600238 A tuple of two dictionaries: The current active kernel state and the
239 inactive kernel state. (Look at A and B constants in this class.)
Alex Klein1699fab2022-09-08 08:46:06 -0600240 """
241 if root_num == self.A[Partition.ROOTFS]:
242 return self.A, self.B
243 elif root_num == self.B[Partition.ROOTFS]:
244 return self.B, self.A
245 else:
246 raise Error(f"Invalid root partition number {root_num}")
Amin Hassanid684e982021-02-26 11:10:58 -0800247
Alex Klein1699fab2022-09-08 08:46:06 -0600248 def _GetMiniOSState(self, minios_num: int) -> Tuple[Dict, Dict]:
249 """Returns the miniOS state.
Amin Hassani75c5f942021-02-20 23:56:53 -0800250
Alex Klein1699fab2022-09-08 08:46:06 -0600251 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600252 A tuple of dictionaries: The current active miniOS state and the
253 inactive miniOS state.
Alex Klein1699fab2022-09-08 08:46:06 -0600254 """
255 if minios_num == self.MINIOS_A[Partition.MINIOS]:
256 return self.MINIOS_A, self.MINIOS_B
257 elif minios_num == self.MINIOS_B[Partition.MINIOS]:
258 return self.MINIOS_B, self.MINIOS_A
259 else:
260 raise Error(f"Invalid minios partition number {minios_num}")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800261
Alex Klein1699fab2022-09-08 08:46:06 -0600262 def _InstallPartitions(self):
263 """The main method that installs the partitions of a Chrome OS device.
Amin Hassani74403082021-02-22 11:40:09 -0800264
Alex Klein1699fab2022-09-08 08:46:06 -0600265 It uses parallelism to install the partitions as fast as possible.
266 """
267 prefix, root_num = self._SplitDevPath(self._device.root_dev)
268 active_state, self._inactive_state = self._GetKernelState(root_num)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000269
Alex Klein1699fab2022-09-08 08:46:06 -0600270 updaters = []
271 if not self._no_rootfs_update:
272 current_root = prefix + str(active_state[Partition.ROOTFS])
273 target_root = prefix + str(self._inactive_state[Partition.ROOTFS])
274 updaters.append(
275 RootfsUpdater(
276 current_root,
277 self._device,
278 self._image,
279 self._image_type,
280 target_root,
281 )
282 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800283
Alex Klein1699fab2022-09-08 08:46:06 -0600284 target_kernel = prefix + str(self._inactive_state[Partition.KERNEL])
285 updaters.append(
286 KernelUpdater(
287 self._device, self._image, self._image_type, target_kernel
288 )
289 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800290
Alex Klein1699fab2022-09-08 08:46:06 -0600291 if not self._no_stateful_update:
292 updaters.append(
293 StatefulUpdater(
294 self._clobber_stateful,
295 self._device,
296 self._image,
297 self._image_type,
298 None,
299 )
300 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800301
Alex Klein1699fab2022-09-08 08:46:06 -0600302 if not self._no_minios_update:
303 minios_priority = self._device.run(
304 ["crossystem", constants.MINIOS_PRIORITY]
305 ).stdout
306 if minios_priority not in ["A", "B"]:
307 logging.warning(
308 "Skipping miniOS flash due to missing priority."
309 )
310 else:
311 # Reference disk_layout_v3 for partition numbering.
312 _, inactive_minios_state = self._GetMiniOSState(
313 9 if minios_priority == "A" else 10
314 )
315 target_minios = prefix + str(
316 inactive_minios_state[Partition.MINIOS]
317 )
318 minios_updater = MiniOSUpdater(
319 self._device, self._image, self._image_type, target_minios
320 )
321 updaters.append(minios_updater)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800322
Alex Klein975e86c2023-01-23 16:49:10 -0700323 # Retry the partitions updates that failed, in case a transient error
324 # (like SSH drop, etc) caused the error.
Alex Klein1699fab2022-09-08 08:46:06 -0600325 num_retries = 1
326 try:
327 retry_util.RetryException(
328 Error,
329 num_retries,
330 parallel.RunParallelSteps,
331 (x.Run for x in updaters if not x.IsFinished()),
332 halt_on_error=True,
333 )
334 except Exception:
Alex Klein975e86c2023-01-23 16:49:10 -0700335 # If one of the partitions failed to be installed, revert all
336 # partitions.
Alex Klein1699fab2022-09-08 08:46:06 -0600337 parallel.RunParallelSteps(x.Revert for x in updaters)
338 raise
339
340 def _Reboot(self):
341 """Reboots the device."""
342 try:
343 self._device.Reboot(timeout_sec=300)
344 except remote_access.RebootError:
345 raise Error(
346 "Could not recover from reboot. Once example reason"
347 " could be the image provided was a non-test image"
348 " or the system failed to boot after the update."
349 )
350 except Exception as e:
351 raise Error(f"Failed to reboot to the device with error: {e}")
352
353 def _VerifyBootExpectations(self):
354 """Verify that we fully booted into the expected kernel state."""
355 # Discover the newly active kernel.
356 _, root_num = self._SplitDevPath(self._device.root_dev)
357 active_state, _ = self._GetKernelState(root_num)
358
359 # If this happens, we should rollback.
360 if active_state != self._inactive_state:
361 raise Error("The expected kernel state after update is invalid.")
362
363 logging.info("Verified boot expectations.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800364
365
366class ReaderBase(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600367 """The base class for reading different inputs and writing into output.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800368
Alex Klein975e86c2023-01-23 16:49:10 -0700369 This class extends threading.Thread, so it will be run on its own thread.
370 Also it can be used as a context manager. Internally, it opens necessary
371 files for writing to and reading from. This class cannot be instantiated, it
372 needs to be sub-classed first to provide necessary function implementations.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800373 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800374
Alex Klein1699fab2022-09-08 08:46:06 -0600375 def __init__(self, use_named_pipes: bool = False):
376 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800377
Alex Klein1699fab2022-09-08 08:46:06 -0600378 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600379 use_named_pipes: Whether to use a named pipe or anonymous file
Alex Klein1699fab2022-09-08 08:46:06 -0600380 descriptors.
381 """
382 super().__init__()
383 self._use_named_pipes = use_named_pipes
384 self._pipe_target = None
385 self._pipe_source = None
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800386
Alex Klein1699fab2022-09-08 08:46:06 -0600387 def __del__(self):
388 """Destructor.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800389
Alex Klein1699fab2022-09-08 08:46:06 -0600390 Make sure to clean up any named pipes we might have created.
391 """
392 if self._use_named_pipes:
393 osutils.SafeUnlink(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800394
Alex Klein1699fab2022-09-08 08:46:06 -0600395 def __enter__(self):
396 """Enters the context manager"""
397 if self._use_named_pipes:
Alex Klein975e86c2023-01-23 16:49:10 -0700398 # There is no need for the temp file, we only need its path. So the
399 # named pipe is created after this temp file is deleted.
Alex Klein1699fab2022-09-08 08:46:06 -0600400 with tempfile.NamedTemporaryFile(
401 prefix="chromite-device-imager"
402 ) as fp:
403 self._pipe_target = self._pipe_source = fp.name
404 os.mkfifo(self._pipe_target)
405 else:
406 self._pipe_target, self._pipe_source = os.pipe()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800407
Alex Klein1699fab2022-09-08 08:46:06 -0600408 self.start()
409 return self
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800410
Alex Klein1699fab2022-09-08 08:46:06 -0600411 def __exit__(self, *args, **kwargs):
412 """Exits the context manager."""
413 self.join()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800414
Alex Klein1699fab2022-09-08 08:46:06 -0600415 def _Source(self):
416 """Returns the source pipe to write data into.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800417
Alex Klein1699fab2022-09-08 08:46:06 -0600418 Sub-classes can use this function to determine where to write their data
419 into.
420 """
421 return self._pipe_source
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800422
Alex Klein1699fab2022-09-08 08:46:06 -0600423 def _CloseSource(self):
424 """Closes the source pipe.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800425
Alex Klein975e86c2023-01-23 16:49:10 -0700426 Sub-classes should use this function to close the pipe after they are
427 done writing into it. Failure to do so may result reader of the data to
428 hang indefinitely.
Alex Klein1699fab2022-09-08 08:46:06 -0600429 """
430 if not self._use_named_pipes:
431 os.close(self._pipe_source)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800432
Alex Klein1699fab2022-09-08 08:46:06 -0600433 def Target(self):
434 """Returns the target pipe to read data from.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800435
Alex Klein1699fab2022-09-08 08:46:06 -0600436 Users of this class can use this path to read data from.
437 """
438 return self._pipe_target
439
440 def CloseTarget(self):
441 """Closes the target pipe.
442
Alex Klein975e86c2023-01-23 16:49:10 -0700443 Users of this class should use this function to close the pipe after
444 they are done reading from it.
Alex Klein1699fab2022-09-08 08:46:06 -0600445 """
446 if self._use_named_pipes:
447 os.remove(self._pipe_target)
448 else:
449 os.close(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800450
451
452class PartialFileReader(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600453 """A class to read specific offset and length from a file and compress it.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800454
Alex Klein1699fab2022-09-08 08:46:06 -0600455 This class can be used to read from specific location and length in a file
Alex Klein975e86c2023-01-23 16:49:10 -0700456 (e.g. A partition in a GPT image). Then it compresses the input and writes
457 it out (to a pipe). Look at the base class for more information.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800458 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800459
Alex Klein1699fab2022-09-08 08:46:06 -0600460 # The offset of different partitions in a Chromium OS image does not always
Alex Klein975e86c2023-01-23 16:49:10 -0700461 # align to larger values like 4096. It seems that 512 is the maximum value
462 # to be divisible by partition offsets. This size should not be increased
463 # just for 'performance reasons'. Since we are doing everything in parallel,
464 # in practice there is not much difference between this and larger block
465 # sizes as parallelism hides the possible extra latency provided by smaller
466 # block sizes.
Alex Klein1699fab2022-09-08 08:46:06 -0600467 _BLOCK_SIZE = 512
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800468
Alex Klein1699fab2022-09-08 08:46:06 -0600469 def __init__(
470 self,
471 image: str,
472 offset: int,
473 length: int,
474 compression_command: List[str],
475 ):
476 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800477
Alex Klein1699fab2022-09-08 08:46:06 -0600478 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600479 image: The path to an image (local or remote directory).
480 offset: The offset (in bytes) to read from the image.
481 length: The length (in bytes) to read from the image.
482 compression_command: The command to compress transferred bytes.
Alex Klein1699fab2022-09-08 08:46:06 -0600483 """
484 super().__init__()
485
486 self._image = image
487 self._offset = offset
488 self._length = length
489 self._compression_command = compression_command
490
491 def run(self):
492 """Runs the reading and compression."""
Mike Frysinger906119e2022-12-27 18:10:23 -0500493 data = osutils.ReadFile(
494 self._image, mode="rb", size=self._length, seek=self._offset
495 )
Alex Klein1699fab2022-09-08 08:46:06 -0600496 try:
Mike Frysinger906119e2022-12-27 18:10:23 -0500497 cros_build_lib.run(
498 self._compression_command, input=data, stdout=self._Source()
499 )
Alex Klein1699fab2022-09-08 08:46:06 -0600500 finally:
501 self._CloseSource()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800502
503
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800504class GsFileCopier(ReaderBase):
Alex Klein975e86c2023-01-23 16:49:10 -0700505 """A class to download gzip compressed file from GS bucket into a pipe."""
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800506
Alex Klein1699fab2022-09-08 08:46:06 -0600507 def __init__(self, image: str):
508 """Initializes the class.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800509
Alex Klein1699fab2022-09-08 08:46:06 -0600510 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600511 image: The path to an image (local or remote directory).
Alex Klein1699fab2022-09-08 08:46:06 -0600512 """
513 super().__init__(use_named_pipes=True)
514 self._image = image
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800515
Alex Klein1699fab2022-09-08 08:46:06 -0600516 def run(self):
517 """Runs the download and write into the output pipe."""
518 try:
519 gs.GSContext().Copy(self._image, self._Source())
520 finally:
521 self._CloseSource()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800522
523
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800524class PartitionUpdaterBase(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600525 """A base abstract class to use for installing an image into a partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800526
Alex Klein1699fab2022-09-08 08:46:06 -0600527 Sub-classes should implement the abstract methods to provide the core
528 functionality.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800529 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800530
Alex Klein1699fab2022-09-08 08:46:06 -0600531 def __init__(self, device, image: str, image_type, target: str):
Alex Klein975e86c2023-01-23 16:49:10 -0700532 """Initializes this base class with the most commonly needed values.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800533
Alex Klein1699fab2022-09-08 08:46:06 -0600534 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600535 device: The ChromiumOSDevice to be updated.
536 image: The target image path for the partition update.
537 image_type: The type of the image (ImageType).
538 target: The target path (e.g. block dev) to install the update.
Alex Klein1699fab2022-09-08 08:46:06 -0600539 """
540 self._device = device
541 self._image = image
542 self._image_type = image_type
543 self._target = target
544 self._finished = False
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800545
Alex Klein1699fab2022-09-08 08:46:06 -0600546 def Run(self):
547 """The main function that does the partition update job."""
548 with timer.Timer() as t:
549 try:
550 self._Run()
551 finally:
552 self._finished = True
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800553
Alex Klein1699fab2022-09-08 08:46:06 -0600554 logging.debug("Completed %s in %s", self.__class__.__name__, t)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800555
Alex Klein1699fab2022-09-08 08:46:06 -0600556 @abc.abstractmethod
557 def _Run(self):
558 """The method that need to be implemented by sub-classes."""
559 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800560
Alex Klein1699fab2022-09-08 08:46:06 -0600561 def IsFinished(self):
562 """Returns whether the partition update has been successful."""
563 return self._finished
564
565 @abc.abstractmethod
566 def Revert(self):
567 """Reverts the partition update.
568
Alex Klein975e86c2023-01-23 16:49:10 -0700569 Subclasses need to implement this function to provide revert capability.
Alex Klein1699fab2022-09-08 08:46:06 -0600570 """
571 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800572
573
574class RawPartitionUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600575 """A class to update a raw partition on a Chromium OS device."""
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800576
Alex Klein1699fab2022-09-08 08:46:06 -0600577 def _Run(self):
578 """The function that does the job of kernel partition update."""
579 if self._image_type == ImageType.FULL:
580 self._CopyPartitionFromImage(self._GetPartitionName())
581 elif self._image_type == ImageType.REMOTE_DIRECTORY:
582 self._RedirectPartition(self._GetRemotePartitionName())
583 else:
584 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800585
Alex Klein1699fab2022-09-08 08:46:06 -0600586 def _GetPartitionName(self):
587 """Returns the name of the partition in a Chromium OS GPT layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800588
Alex Klein1699fab2022-09-08 08:46:06 -0600589 Subclasses should override this function to return correct name.
590 """
591 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800592
Alex Klein1699fab2022-09-08 08:46:06 -0600593 def _CopyPartitionFromImage(self, part_name: str):
594 """Updates the device's partition from a local Chromium OS image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800595
Alex Klein1699fab2022-09-08 08:46:06 -0600596 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600597 part_name: The name of the partition in the source image that needs
598 to be extracted.
Alex Klein1699fab2022-09-08 08:46:06 -0600599 """
600 offset, length = self._GetPartLocation(part_name)
601 offset, length = self._OptimizePartLocation(offset, length)
602 compressor, decompressor = self._GetCompressionAndDecompression()
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900603
Alex Klein1699fab2022-09-08 08:46:06 -0600604 with PartialFileReader(
605 self._image, offset, length, compressor
606 ) as generator:
607 try:
608 self._WriteToTarget(generator.Target(), decompressor)
609 finally:
610 generator.CloseTarget()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800611
Alex Klein1699fab2022-09-08 08:46:06 -0600612 def _GetCompressionAndDecompression(self) -> Tuple[List[str], List[str]]:
613 """Returns compression / decompression commands."""
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900614
Alex Klein1699fab2022-09-08 08:46:06 -0600615 return (
Mike Frysinger66306012022-04-22 15:23:13 -0400616 [
617 cros_build_lib.FindCompressor(
618 cros_build_lib.CompressionType.GZIP
619 )
620 ],
621 self._device.GetDecompressor(cros_build_lib.CompressionType.GZIP),
Alex Klein1699fab2022-09-08 08:46:06 -0600622 )
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900623
Alex Klein1699fab2022-09-08 08:46:06 -0600624 def _WriteToTarget(
625 self, source: Union[int, BytesIO], decompress_command: List[str]
626 ) -> None:
627 """Writes bytes source to the target device on DUT.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800628
Alex Klein1699fab2022-09-08 08:46:06 -0600629 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600630 A string command to run on a device to read data from stdin,
631 uncompress it and write it to the target partition.
Alex Klein1699fab2022-09-08 08:46:06 -0600632 """
Alex Klein1699fab2022-09-08 08:46:06 -0600633 cmd = " ".join(
634 [
635 *decompress_command,
636 "|",
637 "dd",
638 "bs=1M",
Alex Klein1699fab2022-09-08 08:46:06 -0600639 f"of={self._target}",
640 ]
641 )
642 self._device.run(cmd, input=source, shell=True)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800643
Alex Klein1699fab2022-09-08 08:46:06 -0600644 def _GetPartLocation(self, part_name: str):
645 """Extracts the location and size of the raw partition from the image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800646
Alex Klein1699fab2022-09-08 08:46:06 -0600647 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600648 part_name: The name of the partition in the source image that needs
649 to be extracted.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800650
Alex Klein1699fab2022-09-08 08:46:06 -0600651 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600652 A tuple of offset and length (in bytes) from the image.
Alex Klein1699fab2022-09-08 08:46:06 -0600653 """
654 try:
655 parts = image_lib.GetImageDiskPartitionInfo(self._image)
656 part_info = [p for p in parts if p.name == part_name][0]
657 except IndexError:
658 raise Error(f"No partition named {part_name} found.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800659
Alex Klein1699fab2022-09-08 08:46:06 -0600660 return int(part_info.start), int(part_info.size)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800661
Alex Klein1699fab2022-09-08 08:46:06 -0600662 def _GetRemotePartitionName(self):
663 """Returns the name of the quick-provision partition file.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800664
Alex Klein1699fab2022-09-08 08:46:06 -0600665 Subclasses should override this function to return correct name.
666 """
667 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800668
Alex Klein1699fab2022-09-08 08:46:06 -0600669 def _OptimizePartLocation(self, offset: int, length: int):
670 """Optimizes the offset and length of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800671
Alex Klein975e86c2023-01-23 16:49:10 -0700672 Subclasses can override this to provide better offset/length than what
673 is defined in the PGT partition layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800674
Alex Klein1699fab2022-09-08 08:46:06 -0600675 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600676 offset: The offset (in bytes) of the partition in the image.
677 length: The length (in bytes) of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800678
Alex Klein1699fab2022-09-08 08:46:06 -0600679 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600680 A tuple of offset and length (in bytes) from the image.
Alex Klein1699fab2022-09-08 08:46:06 -0600681 """
682 return offset, length
Amin Hassanid684e982021-02-26 11:10:58 -0800683
Alex Klein1699fab2022-09-08 08:46:06 -0600684 def _RedirectPartition(self, file_name: str):
685 """Downloads the partition from a remote path and writes it into target.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800686
Alex Klein1699fab2022-09-08 08:46:06 -0600687 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600688 file_name: The file name in the remote directory self._image.
Alex Klein1699fab2022-09-08 08:46:06 -0600689 """
690 image_path = os.path.join(self._image, file_name)
691 with GsFileCopier(image_path) as generator:
692 try:
693 with open(generator.Target(), "rb") as fp:
694 # Always use GZIP as remote quick provision images are gzip
695 # compressed only.
696 self._WriteToTarget(
697 fp,
Mike Frysinger66306012022-04-22 15:23:13 -0400698 self._device.GetDecompressor(
699 cros_build_lib.CompressionType.GZIP
700 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600701 )
702 finally:
703 generator.CloseTarget()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800704
Amin Hassanid684e982021-02-26 11:10:58 -0800705
706class KernelUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600707 """A class to update the kernel partition on a Chromium OS device."""
Amin Hassanid684e982021-02-26 11:10:58 -0800708
Alex Klein1699fab2022-09-08 08:46:06 -0600709 def _GetPartitionName(self):
710 """See RawPartitionUpdater._GetPartitionName()."""
711 return constants.PART_KERN_B
Amin Hassanid684e982021-02-26 11:10:58 -0800712
Alex Klein1699fab2022-09-08 08:46:06 -0600713 def _GetRemotePartitionName(self):
714 """See RawPartitionUpdater._GetRemotePartitionName()."""
715 return constants.QUICK_PROVISION_PAYLOAD_KERNEL
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800716
Alex Klein1699fab2022-09-08 08:46:06 -0600717 def Revert(self):
718 """Reverts the kernel partition update."""
719 # There is nothing to do for reverting kernel partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800720
721
722class RootfsUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600723 """A class to update the root partition on a Chromium OS device."""
Amin Hassani75c5f942021-02-20 23:56:53 -0800724
Alex Klein1699fab2022-09-08 08:46:06 -0600725 def __init__(self, current_root: str, *args):
726 """Initializes the class.
Amin Hassani75c5f942021-02-20 23:56:53 -0800727
Alex Klein1699fab2022-09-08 08:46:06 -0600728 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600729 current_root: The current root device path.
730 *args: See PartitionUpdaterBase
Alex Klein1699fab2022-09-08 08:46:06 -0600731 """
732 super().__init__(*args)
Amin Hassani75c5f942021-02-20 23:56:53 -0800733
Alex Klein1699fab2022-09-08 08:46:06 -0600734 self._current_root = current_root
735 self._ran_postinst = False
Amin Hassani75c5f942021-02-20 23:56:53 -0800736
Alex Klein1699fab2022-09-08 08:46:06 -0600737 def _GetPartitionName(self):
738 """See RawPartitionUpdater._GetPartitionName()."""
739 return constants.PART_ROOT_A
Amin Hassani75c5f942021-02-20 23:56:53 -0800740
Alex Klein1699fab2022-09-08 08:46:06 -0600741 def _GetRemotePartitionName(self):
742 """See RawPartitionUpdater._GetRemotePartitionName()."""
743 return constants.QUICK_PROVISION_PAYLOAD_ROOTFS
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800744
Alex Klein1699fab2022-09-08 08:46:06 -0600745 def _Run(self):
746 """The function that does the job of rootfs partition update."""
747 with ProgressWatcher(self._device, self._target):
748 super()._Run()
Amin Hassani75c5f942021-02-20 23:56:53 -0800749
Alex Klein1699fab2022-09-08 08:46:06 -0600750 self._RunPostInst()
Amin Hassani75c5f942021-02-20 23:56:53 -0800751
Alex Klein1699fab2022-09-08 08:46:06 -0600752 def _OptimizePartLocation(self, offset: int, length: int):
753 """Optimizes the size of the root partition of the image.
Amin Hassani75c5f942021-02-20 23:56:53 -0800754
Alex Klein975e86c2023-01-23 16:49:10 -0700755 Normally the file system does not occupy the entire partition.
756 Furthermore we don't need the verity hash tree at the end of the root
757 file system because postinst will recreate it. This function reads the
758 (approximate) superblock of the ext4 partition and extracts the actual
759 file system size in the root partition.
Alex Klein1699fab2022-09-08 08:46:06 -0600760 """
761 superblock_size = 4096 * 2
762 with open(self._image, "rb") as r:
763 r.seek(offset)
764 with tempfile.NamedTemporaryFile(delete=False) as fp:
765 fp.write(r.read(superblock_size))
766 fp.close()
767 return offset, partition_lib.Ext2FileSystemSize(fp.name)
Amin Hassani75c5f942021-02-20 23:56:53 -0800768
Alex Klein1699fab2022-09-08 08:46:06 -0600769 def _RunPostInst(self, on_target: bool = True):
770 """Runs the postinst process in the root partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800771
Alex Klein1699fab2022-09-08 08:46:06 -0600772 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600773 on_target: If true the postinst is run on the target (inactive)
774 partition. This is used when doing normal updates. If false, the
775 postinst is run on the current (active) partition. This is used
776 when reverting an update.
Alex Klein1699fab2022-09-08 08:46:06 -0600777 """
778 try:
779 postinst_dir = "/"
780 partition = self._current_root
781 if on_target:
782 postinst_dir = self._device.run(
783 ["mktemp", "-d", "-p", self._device.work_dir],
784 capture_output=True,
785 ).stdout.strip()
786 self._device.run(
787 ["mount", "-o", "ro", self._target, postinst_dir]
788 )
789 partition = self._target
Amin Hassani75c5f942021-02-20 23:56:53 -0800790
Alex Klein1699fab2022-09-08 08:46:06 -0600791 self._ran_postinst = True
792 postinst = os.path.join(postinst_dir, "postinst")
793 result = self._device.run(
794 [postinst, partition], capture_output=True
795 )
Amin Hassani75c5f942021-02-20 23:56:53 -0800796
Alex Klein1699fab2022-09-08 08:46:06 -0600797 logging.debug(
798 "Postinst result on %s: \n%s", postinst, result.stdout
799 )
800 # DeviceImagerOperation will look for this log.
801 logging.info("Postinstall completed.")
802 finally:
803 if on_target:
804 self._device.run(["umount", postinst_dir])
Amin Hassani75c5f942021-02-20 23:56:53 -0800805
Alex Klein1699fab2022-09-08 08:46:06 -0600806 def Revert(self):
807 """Reverts the root update install."""
808 logging.info("Reverting the rootfs partition update.")
809 if self._ran_postinst:
Alex Klein975e86c2023-01-23 16:49:10 -0700810 # We don't have to do anything for revert if we haven't changed the
811 # kernel priorities yet.
Alex Klein1699fab2022-09-08 08:46:06 -0600812 self._RunPostInst(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800813
814
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000815class MiniOSUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600816 """A class to update the miniOS partition on a Chromium OS device."""
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000817
Alex Klein1699fab2022-09-08 08:46:06 -0600818 def __init__(self, *args):
819 """Initializes the class.
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000820
Alex Klein1699fab2022-09-08 08:46:06 -0600821 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600822 *args: See PartitionUpdaterBase
Alex Klein1699fab2022-09-08 08:46:06 -0600823 """
824 super().__init__(*args)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000825
Alex Klein1699fab2022-09-08 08:46:06 -0600826 self._ran_postinst = False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000827
Alex Klein1699fab2022-09-08 08:46:06 -0600828 def _GetPartitionName(self):
829 """See RawPartitionUpdater._GetPartitionName()."""
830 return constants.PART_MINIOS_A
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000831
Alex Klein1699fab2022-09-08 08:46:06 -0600832 def _GetRemotePartitionName(self):
833 """See RawPartitionUpdater._GetRemotePartitionName()."""
834 return constants.QUICK_PROVISION_PAYLOAD_MINIOS
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000835
Alex Klein1699fab2022-09-08 08:46:06 -0600836 def _Run(self):
837 """The function that does the job of rootfs partition update."""
838 if self._image_type == ImageType.FULL:
839 if self._MiniOSPartitionsExistInImage():
840 logging.info("Updating miniOS partition from local.")
841 super()._Run()
842 else:
843 logging.warning(
844 "Not updating miniOS partition as it does not exist."
845 )
846 return
847 elif self._image_type == ImageType.REMOTE_DIRECTORY:
848 if not gs.GSContext().Exists(
849 os.path.join(
850 self._image, constants.QUICK_PROVISION_PAYLOAD_MINIOS
851 )
852 ):
853 logging.warning("Not updating miniOS, missing remote files.")
854 return
855 elif not self._MiniOSPartitionsExist():
856 logging.warning("Not updating miniOS, missing partitions.")
857 return
858 else:
859 logging.info("Updating miniOS partition from remote.")
860 super()._Run()
861 else:
862 # Let super() handle this error.
863 super()._Run()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000864
Alex Klein1699fab2022-09-08 08:46:06 -0600865 self._RunPostInstall()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000866
Alex Klein1699fab2022-09-08 08:46:06 -0600867 def _RunPostInstall(self):
868 """The function will change the priority of the miniOS partitions."""
869 self._FlipMiniOSPriority()
870 self._ran_postinst = True
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000871
Alex Klein1699fab2022-09-08 08:46:06 -0600872 def Revert(self):
873 """Reverts the miniOS partition update."""
874 if self._ran_postinst:
875 self._FlipMiniOSPriority()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000876
Alex Klein1699fab2022-09-08 08:46:06 -0600877 def _GetMiniOSPriority(self):
878 return self._device.run(
879 ["crossystem", constants.MINIOS_PRIORITY]
880 ).stdout
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000881
Alex Klein1699fab2022-09-08 08:46:06 -0600882 def _SetMiniOSPriority(self, priority: str):
883 self._device.run(
884 ["crossystem", f"{constants.MINIOS_PRIORITY}={priority}"]
885 )
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000886
Alex Klein1699fab2022-09-08 08:46:06 -0600887 def _FlipMiniOSPriority(self):
888 inactive_minios_priority = (
889 "B" if self._GetMiniOSPriority() == "A" else "A"
890 )
891 logging.info("Setting miniOS priority to %s", inactive_minios_priority)
892 self._SetMiniOSPriority(inactive_minios_priority)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000893
Alex Klein1699fab2022-09-08 08:46:06 -0600894 def _MiniOSPartitionsExistInImage(self):
895 """Checks if miniOS partition exists in the image."""
896 d = cgpt.Disk.FromImage(self._image)
897 try:
898 d.GetPartitionByTypeGuid(cgpt.MINIOS_TYPE_GUID)
899 return True
900 except KeyError:
901 return False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000902
Alex Klein1699fab2022-09-08 08:46:06 -0600903 def _MiniOSPartitionsExist(self):
904 """Checks if the device has miniOS partitions."""
905 run = lambda x: self._device.run(x).stdout.strip()
906 device_drive = run(["rootdev", "-s", "-d"])
907 cmd = ["cgpt", "show", "-t", device_drive, "-i"]
908 return all(
909 (run(cmd + [p]) == cgpt.MINIOS_TYPE_GUID) for p in ("9", "10")
910 )
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700911
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000912
Amin Hassani74403082021-02-22 11:40:09 -0800913class StatefulPayloadGenerator(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600914 """A class for generating a stateful update payload in a separate thread."""
Amin Hassani74403082021-02-22 11:40:09 -0800915
Alex Klein1699fab2022-09-08 08:46:06 -0600916 def __init__(self, image: str):
917 """Initializes that class.
Amin Hassani74403082021-02-22 11:40:09 -0800918
Alex Klein1699fab2022-09-08 08:46:06 -0600919 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600920 image: The path to a local Chromium OS image.
Alex Klein1699fab2022-09-08 08:46:06 -0600921 """
922 super().__init__()
923 self._image = image
924
925 def run(self):
926 """Generates the stateful update and writes it into the output pipe."""
927 try:
928 paygen_stateful_payload_lib.GenerateStatefulPayload(
929 self._image, self._Source()
930 )
931 finally:
932 self._CloseSource()
Amin Hassani74403082021-02-22 11:40:09 -0800933
934
935class StatefulUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600936 """A class to update the stateful partition on a device."""
Amin Hassani74403082021-02-22 11:40:09 -0800937
Alex Klein1699fab2022-09-08 08:46:06 -0600938 def __init__(self, clobber_stateful: bool, *args):
939 """Initializes the class
Amin Hassani74403082021-02-22 11:40:09 -0800940
Alex Klein1699fab2022-09-08 08:46:06 -0600941 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600942 clobber_stateful: Whether to clobber the stateful or not.
943 *args: Look at PartitionUpdaterBase.
Alex Klein1699fab2022-09-08 08:46:06 -0600944 """
945 super().__init__(*args)
946 self._clobber_stateful = clobber_stateful
Amin Hassani74403082021-02-22 11:40:09 -0800947
Alex Klein1699fab2022-09-08 08:46:06 -0600948 def _Run(self):
Alex Klein975e86c2023-01-23 16:49:10 -0700949 """Read/Download the stateful updates and write it into the device."""
Alex Klein1699fab2022-09-08 08:46:06 -0600950 if self._image_type == ImageType.FULL:
951 generator_cls = StatefulPayloadGenerator
952 elif self._image_type == ImageType.REMOTE_DIRECTORY:
953 generator_cls = GsFileCopier
954 self._image = os.path.join(
955 self._image, paygen_stateful_payload_lib.STATEFUL_FILE
956 )
957 else:
958 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassani74403082021-02-22 11:40:09 -0800959
Alex Klein1699fab2022-09-08 08:46:06 -0600960 with generator_cls(self._image) as generator:
961 try:
962 updater = stateful_updater.StatefulUpdater(self._device)
963 updater.Update(
964 generator.Target(),
965 is_payload_on_device=False,
966 update_type=(
967 stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER
968 if self._clobber_stateful
969 else None
970 ),
971 )
972 finally:
973 generator.CloseTarget()
974
975 def Revert(self):
976 """Reverts the stateful partition update."""
977 logging.info("Reverting the stateful update.")
978 stateful_updater.StatefulUpdater(self._device).Reset()
Amin Hassani55970562021-02-22 20:49:13 -0800979
980
981class ProgressWatcher(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600982 """A class used for watching the progress of rootfs update."""
Amin Hassani55970562021-02-22 20:49:13 -0800983
Alex Klein1699fab2022-09-08 08:46:06 -0600984 def __init__(self, device, target_root: str):
985 """Initializes the class.
Amin Hassani55970562021-02-22 20:49:13 -0800986
Alex Klein1699fab2022-09-08 08:46:06 -0600987 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600988 device: The ChromiumOSDevice to be updated.
989 target_root: The target root partition to monitor the progress of.
Alex Klein1699fab2022-09-08 08:46:06 -0600990 """
991 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -0800992
Alex Klein1699fab2022-09-08 08:46:06 -0600993 self._device = device
994 self._target_root = target_root
995 self._exit = False
Amin Hassani55970562021-02-22 20:49:13 -0800996
Alex Klein1699fab2022-09-08 08:46:06 -0600997 def __enter__(self):
998 """Starts the thread."""
999 self.start()
1000 return self
Amin Hassani55970562021-02-22 20:49:13 -08001001
Alex Klein1699fab2022-09-08 08:46:06 -06001002 def __exit__(self, *args, **kwargs):
1003 """Exists the thread."""
1004 self._exit = True
1005 self.join()
Amin Hassani55970562021-02-22 20:49:13 -08001006
Alex Klein1699fab2022-09-08 08:46:06 -06001007 def _ShouldExit(self):
1008 return self._exit
Amin Hassani55970562021-02-22 20:49:13 -08001009
Alex Klein1699fab2022-09-08 08:46:06 -06001010 def run(self):
1011 """Monitors the progress of the target root partitions' update.
Amin Hassani55970562021-02-22 20:49:13 -08001012
Alex Klein975e86c2023-01-23 16:49:10 -07001013 This is done by periodically, reading the fd position of the process
1014 that is writing into the target partition and reporting it back. Then
1015 the position is divided by the size of the block device to report
1016 approximate progress.
Alex Klein1699fab2022-09-08 08:46:06 -06001017 """
1018 cmd = ["blockdev", "--getsize64", self._target_root]
Amin Hassani55970562021-02-22 20:49:13 -08001019 output = self._device.run(cmd, capture_output=True).stdout.strip()
Alex Klein1699fab2022-09-08 08:46:06 -06001020 if output is None:
1021 raise Error(f"Cannot get the block device size from {output}.")
1022 dev_size = int(output)
1023
1024 # Using lsof to find out which process is writing to the target rootfs.
1025 cmd = ["lsof", "-t", self._target_root]
Brian Norris7ceb0fe2022-11-10 17:46:31 -08001026 while True:
1027 if self._ShouldExit():
1028 return
1029
Alex Klein1699fab2022-09-08 08:46:06 -06001030 try:
1031 pid = self._device.run(cmd, capture_output=True).stdout.strip()
1032 if pid:
1033 break
1034 except cros_build_lib.RunCommandError:
1035 continue
1036 finally:
1037 time.sleep(1)
1038
Alex Klein975e86c2023-01-23 16:49:10 -07001039 # Now that we know which process is writing to it, we can look the
1040 # fdinfo of stdout of that process to get its offset. We're assuming
1041 # there will be no seek, which is correct.
Alex Klein1699fab2022-09-08 08:46:06 -06001042 cmd = ["cat", f"/proc/{pid}/fdinfo/1"]
1043 while not self._ShouldExit():
1044 try:
1045 output = self._device.run(
1046 cmd, capture_output=True
1047 ).stdout.strip()
1048 m = re.search(r"^pos:\s*(\d+)$", output, flags=re.M)
1049 if m:
1050 offset = int(m.group(1))
1051 # DeviceImagerOperation will look for this log.
1052 logging.info("RootFS progress: %f", offset / dev_size)
1053 except cros_build_lib.RunCommandError:
1054 continue
1055 finally:
1056 time.sleep(1)
Amin Hassani55970562021-02-22 20:49:13 -08001057
1058
1059class DeviceImagerOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -06001060 """A class to provide a progress bar for DeviceImager operation."""
Amin Hassani55970562021-02-22 20:49:13 -08001061
Alex Klein1699fab2022-09-08 08:46:06 -06001062 def __init__(self):
1063 """Initializes the class."""
1064 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -08001065
Alex Klein1699fab2022-09-08 08:46:06 -06001066 self._progress = 0.0
Amin Hassani55970562021-02-22 20:49:13 -08001067
Alex Klein1699fab2022-09-08 08:46:06 -06001068 def ParseOutput(self, output=None):
1069 """Override function to parse the output and provide progress.
Amin Hassani55970562021-02-22 20:49:13 -08001070
Alex Klein1699fab2022-09-08 08:46:06 -06001071 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001072 output: The stderr or stdout.
Alex Klein1699fab2022-09-08 08:46:06 -06001073 """
1074 output = self._stdout.read()
1075 match = re.findall(r"RootFS progress: (\d+(?:\.\d+)?)", output)
1076 if match:
1077 progress = float(match[0])
1078 self._progress = max(self._progress, progress)
Amin Hassani55970562021-02-22 20:49:13 -08001079
Alex Klein1699fab2022-09-08 08:46:06 -06001080 # If postinstall completes, move half of the remaining progress.
1081 if re.findall(r"Postinstall completed", output):
1082 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001083
Alex Klein975e86c2023-01-23 16:49:10 -07001084 # While waiting for reboot, each time, move half of the remaining
1085 # progress.
Alex Klein1699fab2022-09-08 08:46:06 -06001086 if re.findall(r"Unable to get new boot_id", output):
1087 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001088
Alex Klein1699fab2022-09-08 08:46:06 -06001089 if re.findall(r"DeviceImager completed.", output):
1090 self._progress = 1.0
Amin Hassani55970562021-02-22 20:49:13 -08001091
Alex Klein1699fab2022-09-08 08:46:06 -06001092 self.ProgressBar(self._progress)