blob: e0f82d7bbd04a56e050308363d1840dae10b636f [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 Klein1699fab2022-09-08 08:46:06 -060063 This utility uses parallelism as much as possible to achieve its goal as fast
64 as possible. For example, it uses parallel compressors, parallel transfers,
65 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
167 (e.g. chromiumos_test_image.bin). If the path is remote, it should be the
168 remote directory where we can find the quick-provision and stateful update
169 files (e.g. gs://chromeos-image-archive/eve-release/R90-x.x.x).
Amin Hassanicf8f0042021-03-12 10:42:13 -0800170
Alex Klein1699fab2022-09-08 08:46:06 -0600171 NOTE: At this point there is no caching involved. Hence we always download
172 the partition payloads or extract them from the Chromium OS image.
173 """
174 if os.path.isfile(self._image):
175 self._image_type = ImageType.FULL
176 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800177
Alex Klein1699fab2022-09-08 08:46:06 -0600178 # TODO(b/172212406): We could potentially also allow this by searching
179 # through the directory to see whether we have quick-provision and stateful
180 # payloads. This only makes sense when a user has their workstation at home
181 # and doesn't want to incur the bandwidth cost of downloading the same
182 # image multiple times. For that, they can simply download the GPT image
183 # image first and flash that instead.
184 if os.path.isdir(self._image):
185 raise ValueError(
186 f"{self._image}: input must be a disk image, not a directory."
187 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800188
Alex Klein1699fab2022-09-08 08:46:06 -0600189 if gs.PathIsGs(self._image):
190 # TODO(b/172212406): Check whether it is a directory. If it wasn't a
191 # directory download the image into some temp location and use it instead.
192 self._image_type = ImageType.REMOTE_DIRECTORY
193 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800194
Alex Klein1699fab2022-09-08 08:46:06 -0600195 # Assuming it is an xBuddy path.
196 board = cros_build_lib.GetBoard(
197 device_board=self._device.board or flash.GetDefaultBoard(),
198 override_board=self._board,
199 force=True,
200 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800201
Alex Klein1699fab2022-09-08 08:46:06 -0600202 xb = xbuddy.XBuddy(board=board, version=self._version)
203 build_id, local_file = xb.Translate([self._image])
204 if build_id is None:
205 raise Error(f"{self._image}: unable to find matching xBuddy path.")
206 logging.info("XBuddy path translated to build ID %s", build_id)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800207
Alex Klein1699fab2022-09-08 08:46:06 -0600208 if local_file:
209 self._image = local_file
210 self._image_type = ImageType.FULL
211 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800212
Alex Klein1699fab2022-09-08 08:46:06 -0600213 self._image = f"{devserver_constants.GS_IMAGE_DIR}/{build_id}"
214 self._image_type = ImageType.REMOTE_DIRECTORY
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800215
Alex Klein1699fab2022-09-08 08:46:06 -0600216 def _SplitDevPath(self, path: str) -> Tuple[str, int]:
217 """Splits the given /dev/x path into prefix and the dev number.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800218
Alex Klein1699fab2022-09-08 08:46:06 -0600219 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600220 path: The path to a block dev device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800221
Alex Klein1699fab2022-09-08 08:46:06 -0600222 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600223 A tuple of representing the prefix and the index of the dev path.
224 e.g.: '/dev/mmcblk0p1' -> ['/dev/mmcblk0p', 1]
Alex Klein1699fab2022-09-08 08:46:06 -0600225 """
226 match = re.search(r"(.*)([0-9]+)$", path)
227 if match is None:
228 raise Error(f"{path}: Could not parse root dev path.")
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000229
Alex Klein1699fab2022-09-08 08:46:06 -0600230 return match.group(1), int(match.group(2))
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000231
Alex Klein1699fab2022-09-08 08:46:06 -0600232 def _GetKernelState(self, root_num: int) -> Tuple[Dict, Dict]:
233 """Returns the kernel state.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800234
Alex Klein1699fab2022-09-08 08:46:06 -0600235 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600236 A tuple of two dictionaries: The current active kernel state and the
237 inactive kernel state. (Look at A and B constants in this class.)
Alex Klein1699fab2022-09-08 08:46:06 -0600238 """
239 if root_num == self.A[Partition.ROOTFS]:
240 return self.A, self.B
241 elif root_num == self.B[Partition.ROOTFS]:
242 return self.B, self.A
243 else:
244 raise Error(f"Invalid root partition number {root_num}")
Amin Hassanid684e982021-02-26 11:10:58 -0800245
Alex Klein1699fab2022-09-08 08:46:06 -0600246 def _GetMiniOSState(self, minios_num: int) -> Tuple[Dict, Dict]:
247 """Returns the miniOS state.
Amin Hassani75c5f942021-02-20 23:56:53 -0800248
Alex Klein1699fab2022-09-08 08:46:06 -0600249 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600250 A tuple of dictionaries: The current active miniOS state and the
251 inactive miniOS state.
Alex Klein1699fab2022-09-08 08:46:06 -0600252 """
253 if minios_num == self.MINIOS_A[Partition.MINIOS]:
254 return self.MINIOS_A, self.MINIOS_B
255 elif minios_num == self.MINIOS_B[Partition.MINIOS]:
256 return self.MINIOS_B, self.MINIOS_A
257 else:
258 raise Error(f"Invalid minios partition number {minios_num}")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800259
Alex Klein1699fab2022-09-08 08:46:06 -0600260 def _InstallPartitions(self):
261 """The main method that installs the partitions of a Chrome OS device.
Amin Hassani74403082021-02-22 11:40:09 -0800262
Alex Klein1699fab2022-09-08 08:46:06 -0600263 It uses parallelism to install the partitions as fast as possible.
264 """
265 prefix, root_num = self._SplitDevPath(self._device.root_dev)
266 active_state, self._inactive_state = self._GetKernelState(root_num)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000267
Alex Klein1699fab2022-09-08 08:46:06 -0600268 updaters = []
269 if not self._no_rootfs_update:
270 current_root = prefix + str(active_state[Partition.ROOTFS])
271 target_root = prefix + str(self._inactive_state[Partition.ROOTFS])
272 updaters.append(
273 RootfsUpdater(
274 current_root,
275 self._device,
276 self._image,
277 self._image_type,
278 target_root,
279 )
280 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800281
Alex Klein1699fab2022-09-08 08:46:06 -0600282 target_kernel = prefix + str(self._inactive_state[Partition.KERNEL])
283 updaters.append(
284 KernelUpdater(
285 self._device, self._image, self._image_type, target_kernel
286 )
287 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800288
Alex Klein1699fab2022-09-08 08:46:06 -0600289 if not self._no_stateful_update:
290 updaters.append(
291 StatefulUpdater(
292 self._clobber_stateful,
293 self._device,
294 self._image,
295 self._image_type,
296 None,
297 )
298 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800299
Alex Klein1699fab2022-09-08 08:46:06 -0600300 if not self._no_minios_update:
301 minios_priority = self._device.run(
302 ["crossystem", constants.MINIOS_PRIORITY]
303 ).stdout
304 if minios_priority not in ["A", "B"]:
305 logging.warning(
306 "Skipping miniOS flash due to missing priority."
307 )
308 else:
309 # Reference disk_layout_v3 for partition numbering.
310 _, inactive_minios_state = self._GetMiniOSState(
311 9 if minios_priority == "A" else 10
312 )
313 target_minios = prefix + str(
314 inactive_minios_state[Partition.MINIOS]
315 )
316 minios_updater = MiniOSUpdater(
317 self._device, self._image, self._image_type, target_minios
318 )
319 updaters.append(minios_updater)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800320
Alex Klein1699fab2022-09-08 08:46:06 -0600321 # Retry the partitions updates that failed, in case a transient error (like
322 # SSH drop, etc) caused the error.
323 num_retries = 1
324 try:
325 retry_util.RetryException(
326 Error,
327 num_retries,
328 parallel.RunParallelSteps,
329 (x.Run for x in updaters if not x.IsFinished()),
330 halt_on_error=True,
331 )
332 except Exception:
333 # If one of the partitions failed to be installed, revert all partitions.
334 parallel.RunParallelSteps(x.Revert for x in updaters)
335 raise
336
337 def _Reboot(self):
338 """Reboots the device."""
339 try:
340 self._device.Reboot(timeout_sec=300)
341 except remote_access.RebootError:
342 raise Error(
343 "Could not recover from reboot. Once example reason"
344 " could be the image provided was a non-test image"
345 " or the system failed to boot after the update."
346 )
347 except Exception as e:
348 raise Error(f"Failed to reboot to the device with error: {e}")
349
350 def _VerifyBootExpectations(self):
351 """Verify that we fully booted into the expected kernel state."""
352 # Discover the newly active kernel.
353 _, root_num = self._SplitDevPath(self._device.root_dev)
354 active_state, _ = self._GetKernelState(root_num)
355
356 # If this happens, we should rollback.
357 if active_state != self._inactive_state:
358 raise Error("The expected kernel state after update is invalid.")
359
360 logging.info("Verified boot expectations.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800361
362
363class ReaderBase(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600364 """The base class for reading different inputs and writing into output.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800365
Alex Klein1699fab2022-09-08 08:46:06 -0600366 This class extends threading.Thread, so it will be run on its own thread. Also
367 it can be used as a context manager. Internally, it opens necessary files for
368 writing to and reading from. This class cannot be instantiated, it needs to be
369 sub-classed first to provide necessary function implementations.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800370 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800371
Alex Klein1699fab2022-09-08 08:46:06 -0600372 def __init__(self, use_named_pipes: bool = False):
373 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800374
Alex Klein1699fab2022-09-08 08:46:06 -0600375 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600376 use_named_pipes: Whether to use a named pipe or anonymous file
Alex Klein1699fab2022-09-08 08:46:06 -0600377 descriptors.
378 """
379 super().__init__()
380 self._use_named_pipes = use_named_pipes
381 self._pipe_target = None
382 self._pipe_source = None
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800383
Alex Klein1699fab2022-09-08 08:46:06 -0600384 def __del__(self):
385 """Destructor.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800386
Alex Klein1699fab2022-09-08 08:46:06 -0600387 Make sure to clean up any named pipes we might have created.
388 """
389 if self._use_named_pipes:
390 osutils.SafeUnlink(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800391
Alex Klein1699fab2022-09-08 08:46:06 -0600392 def __enter__(self):
393 """Enters the context manager"""
394 if self._use_named_pipes:
395 # There is no need for the temp file, we only need its path. So the named
396 # pipe is created after this temp file is deleted.
397 with tempfile.NamedTemporaryFile(
398 prefix="chromite-device-imager"
399 ) as fp:
400 self._pipe_target = self._pipe_source = fp.name
401 os.mkfifo(self._pipe_target)
402 else:
403 self._pipe_target, self._pipe_source = os.pipe()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800404
Alex Klein1699fab2022-09-08 08:46:06 -0600405 self.start()
406 return self
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800407
Alex Klein1699fab2022-09-08 08:46:06 -0600408 def __exit__(self, *args, **kwargs):
409 """Exits the context manager."""
410 self.join()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800411
Alex Klein1699fab2022-09-08 08:46:06 -0600412 def _Source(self):
413 """Returns the source pipe to write data into.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800414
Alex Klein1699fab2022-09-08 08:46:06 -0600415 Sub-classes can use this function to determine where to write their data
416 into.
417 """
418 return self._pipe_source
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800419
Alex Klein1699fab2022-09-08 08:46:06 -0600420 def _CloseSource(self):
421 """Closes the source pipe.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800422
Alex Klein1699fab2022-09-08 08:46:06 -0600423 Sub-classes should use this function to close the pipe after they are done
424 writing into it. Failure to do so may result reader of the data to hang
425 indefinitely.
426 """
427 if not self._use_named_pipes:
428 os.close(self._pipe_source)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800429
Alex Klein1699fab2022-09-08 08:46:06 -0600430 def Target(self):
431 """Returns the target pipe to read data from.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800432
Alex Klein1699fab2022-09-08 08:46:06 -0600433 Users of this class can use this path to read data from.
434 """
435 return self._pipe_target
436
437 def CloseTarget(self):
438 """Closes the target pipe.
439
440 Users of this class should use this function to close the pipe after they
441 are done reading from it.
442 """
443 if self._use_named_pipes:
444 os.remove(self._pipe_target)
445 else:
446 os.close(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800447
448
449class PartialFileReader(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600450 """A class to read specific offset and length from a file and compress it.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800451
Alex Klein1699fab2022-09-08 08:46:06 -0600452 This class can be used to read from specific location and length in a file
453 (e.g. A partition in a GPT image). Then it compresses the input and writes it
454 out (to a pipe). Look at the base class for more information.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800455 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800456
Alex Klein1699fab2022-09-08 08:46:06 -0600457 # The offset of different partitions in a Chromium OS image does not always
458 # align to larger values like 4096. It seems that 512 is the maximum value to
459 # be divisible by partition offsets. This size should not be increased just
460 # for 'performance reasons'. Since we are doing everything in parallel, in
461 # practice there is not much difference between this and larger block sizes as
462 # parallelism hides the possible extra latency provided by smaller block
463 # sizes.
464 _BLOCK_SIZE = 512
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800465
Alex Klein1699fab2022-09-08 08:46:06 -0600466 def __init__(
467 self,
468 image: str,
469 offset: int,
470 length: int,
471 compression_command: List[str],
472 ):
473 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800474
Alex Klein1699fab2022-09-08 08:46:06 -0600475 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600476 image: The path to an image (local or remote directory).
477 offset: The offset (in bytes) to read from the image.
478 length: The length (in bytes) to read from the image.
479 compression_command: The command to compress transferred bytes.
Alex Klein1699fab2022-09-08 08:46:06 -0600480 """
481 super().__init__()
482
483 self._image = image
484 self._offset = offset
485 self._length = length
486 self._compression_command = compression_command
487
488 def run(self):
489 """Runs the reading and compression."""
Mike Frysinger906119e2022-12-27 18:10:23 -0500490 data = osutils.ReadFile(
491 self._image, mode="rb", size=self._length, seek=self._offset
492 )
Alex Klein1699fab2022-09-08 08:46:06 -0600493 try:
Mike Frysinger906119e2022-12-27 18:10:23 -0500494 cros_build_lib.run(
495 self._compression_command, input=data, stdout=self._Source()
496 )
Alex Klein1699fab2022-09-08 08:46:06 -0600497 finally:
498 self._CloseSource()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800499
500
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800501class GsFileCopier(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600502 """A class for downloading gzip compressed file from GS bucket into a pipe."""
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800503
Alex Klein1699fab2022-09-08 08:46:06 -0600504 def __init__(self, image: str):
505 """Initializes the class.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800506
Alex Klein1699fab2022-09-08 08:46:06 -0600507 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600508 image: The path to an image (local or remote directory).
Alex Klein1699fab2022-09-08 08:46:06 -0600509 """
510 super().__init__(use_named_pipes=True)
511 self._image = image
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800512
Alex Klein1699fab2022-09-08 08:46:06 -0600513 def run(self):
514 """Runs the download and write into the output pipe."""
515 try:
516 gs.GSContext().Copy(self._image, self._Source())
517 finally:
518 self._CloseSource()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800519
520
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800521class PartitionUpdaterBase(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600522 """A base abstract class to use for installing an image into a partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800523
Alex Klein1699fab2022-09-08 08:46:06 -0600524 Sub-classes should implement the abstract methods to provide the core
525 functionality.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800526 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800527
Alex Klein1699fab2022-09-08 08:46:06 -0600528 def __init__(self, device, image: str, image_type, target: str):
529 """Initializes this base class with values that most sub-classes will need.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800530
Alex Klein1699fab2022-09-08 08:46:06 -0600531 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600532 device: The ChromiumOSDevice to be updated.
533 image: The target image path for the partition update.
534 image_type: The type of the image (ImageType).
535 target: The target path (e.g. block dev) to install the update.
Alex Klein1699fab2022-09-08 08:46:06 -0600536 """
537 self._device = device
538 self._image = image
539 self._image_type = image_type
540 self._target = target
541 self._finished = False
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800542
Alex Klein1699fab2022-09-08 08:46:06 -0600543 def Run(self):
544 """The main function that does the partition update job."""
545 with timer.Timer() as t:
546 try:
547 self._Run()
548 finally:
549 self._finished = True
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800550
Alex Klein1699fab2022-09-08 08:46:06 -0600551 logging.debug("Completed %s in %s", self.__class__.__name__, t)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800552
Alex Klein1699fab2022-09-08 08:46:06 -0600553 @abc.abstractmethod
554 def _Run(self):
555 """The method that need to be implemented by sub-classes."""
556 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800557
Alex Klein1699fab2022-09-08 08:46:06 -0600558 def IsFinished(self):
559 """Returns whether the partition update has been successful."""
560 return self._finished
561
562 @abc.abstractmethod
563 def Revert(self):
564 """Reverts the partition update.
565
566 Sub-classes need to implement this function to provide revert capability.
567 """
568 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800569
570
571class RawPartitionUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600572 """A class to update a raw partition on a Chromium OS device."""
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800573
Alex Klein1699fab2022-09-08 08:46:06 -0600574 def _Run(self):
575 """The function that does the job of kernel partition update."""
576 if self._image_type == ImageType.FULL:
577 self._CopyPartitionFromImage(self._GetPartitionName())
578 elif self._image_type == ImageType.REMOTE_DIRECTORY:
579 self._RedirectPartition(self._GetRemotePartitionName())
580 else:
581 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800582
Alex Klein1699fab2022-09-08 08:46:06 -0600583 def _GetPartitionName(self):
584 """Returns the name of the partition in a Chromium OS GPT layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800585
Alex Klein1699fab2022-09-08 08:46:06 -0600586 Subclasses should override this function to return correct name.
587 """
588 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800589
Alex Klein1699fab2022-09-08 08:46:06 -0600590 def _CopyPartitionFromImage(self, part_name: str):
591 """Updates the device's partition from a local Chromium OS image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800592
Alex Klein1699fab2022-09-08 08:46:06 -0600593 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600594 part_name: The name of the partition in the source image that needs
595 to be extracted.
Alex Klein1699fab2022-09-08 08:46:06 -0600596 """
597 offset, length = self._GetPartLocation(part_name)
598 offset, length = self._OptimizePartLocation(offset, length)
599 compressor, decompressor = self._GetCompressionAndDecompression()
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900600
Alex Klein1699fab2022-09-08 08:46:06 -0600601 with PartialFileReader(
602 self._image, offset, length, compressor
603 ) as generator:
604 try:
605 self._WriteToTarget(generator.Target(), decompressor)
606 finally:
607 generator.CloseTarget()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800608
Alex Klein1699fab2022-09-08 08:46:06 -0600609 def _GetCompressionAndDecompression(self) -> Tuple[List[str], List[str]]:
610 """Returns compression / decompression commands."""
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900611
Alex Klein1699fab2022-09-08 08:46:06 -0600612 return (
Mike Frysinger66306012022-04-22 15:23:13 -0400613 [
614 cros_build_lib.FindCompressor(
615 cros_build_lib.CompressionType.GZIP
616 )
617 ],
618 self._device.GetDecompressor(cros_build_lib.CompressionType.GZIP),
Alex Klein1699fab2022-09-08 08:46:06 -0600619 )
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900620
Alex Klein1699fab2022-09-08 08:46:06 -0600621 def _WriteToTarget(
622 self, source: Union[int, BytesIO], decompress_command: List[str]
623 ) -> None:
624 """Writes bytes source to the target device on DUT.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800625
Alex Klein1699fab2022-09-08 08:46:06 -0600626 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600627 A string command to run on a device to read data from stdin,
628 uncompress it and write it to the target partition.
Alex Klein1699fab2022-09-08 08:46:06 -0600629 """
630 # Using oflag=direct to tell the OS not to cache the writes (faster).
631 cmd = " ".join(
632 [
633 *decompress_command,
634 "|",
635 "dd",
636 "bs=1M",
637 "oflag=direct",
638 f"of={self._target}",
639 ]
640 )
641 self._device.run(cmd, input=source, shell=True)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800642
Alex Klein1699fab2022-09-08 08:46:06 -0600643 def _GetPartLocation(self, part_name: str):
644 """Extracts the location and size of the raw partition from the image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800645
Alex Klein1699fab2022-09-08 08:46:06 -0600646 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600647 part_name: The name of the partition in the source image that needs
648 to be extracted.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800649
Alex Klein1699fab2022-09-08 08:46:06 -0600650 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600651 A tuple of offset and length (in bytes) from the image.
Alex Klein1699fab2022-09-08 08:46:06 -0600652 """
653 try:
654 parts = image_lib.GetImageDiskPartitionInfo(self._image)
655 part_info = [p for p in parts if p.name == part_name][0]
656 except IndexError:
657 raise Error(f"No partition named {part_name} found.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800658
Alex Klein1699fab2022-09-08 08:46:06 -0600659 return int(part_info.start), int(part_info.size)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800660
Alex Klein1699fab2022-09-08 08:46:06 -0600661 def _GetRemotePartitionName(self):
662 """Returns the name of the quick-provision partition file.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800663
Alex Klein1699fab2022-09-08 08:46:06 -0600664 Subclasses should override this function to return correct name.
665 """
666 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800667
Alex Klein1699fab2022-09-08 08:46:06 -0600668 def _OptimizePartLocation(self, offset: int, length: int):
669 """Optimizes the offset and length of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800670
Alex Klein1699fab2022-09-08 08:46:06 -0600671 Subclasses can override this to provide better offset/length than what is
672 defined in the PGT partition layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800673
Alex Klein1699fab2022-09-08 08:46:06 -0600674 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600675 offset: The offset (in bytes) of the partition in the image.
676 length: The length (in bytes) of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800677
Alex Klein1699fab2022-09-08 08:46:06 -0600678 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600679 A tuple of offset and length (in bytes) from the image.
Alex Klein1699fab2022-09-08 08:46:06 -0600680 """
681 return offset, length
Amin Hassanid684e982021-02-26 11:10:58 -0800682
Alex Klein1699fab2022-09-08 08:46:06 -0600683 def _RedirectPartition(self, file_name: str):
684 """Downloads the partition from a remote path and writes it into target.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800685
Alex Klein1699fab2022-09-08 08:46:06 -0600686 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600687 file_name: The file name in the remote directory self._image.
Alex Klein1699fab2022-09-08 08:46:06 -0600688 """
689 image_path = os.path.join(self._image, file_name)
690 with GsFileCopier(image_path) as generator:
691 try:
692 with open(generator.Target(), "rb") as fp:
693 # Always use GZIP as remote quick provision images are gzip
694 # compressed only.
695 self._WriteToTarget(
696 fp,
Mike Frysinger66306012022-04-22 15:23:13 -0400697 self._device.GetDecompressor(
698 cros_build_lib.CompressionType.GZIP
699 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600700 )
701 finally:
702 generator.CloseTarget()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800703
Amin Hassanid684e982021-02-26 11:10:58 -0800704
705class KernelUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600706 """A class to update the kernel partition on a Chromium OS device."""
Amin Hassanid684e982021-02-26 11:10:58 -0800707
Alex Klein1699fab2022-09-08 08:46:06 -0600708 def _GetPartitionName(self):
709 """See RawPartitionUpdater._GetPartitionName()."""
710 return constants.PART_KERN_B
Amin Hassanid684e982021-02-26 11:10:58 -0800711
Alex Klein1699fab2022-09-08 08:46:06 -0600712 def _GetRemotePartitionName(self):
713 """See RawPartitionUpdater._GetRemotePartitionName()."""
714 return constants.QUICK_PROVISION_PAYLOAD_KERNEL
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800715
Alex Klein1699fab2022-09-08 08:46:06 -0600716 def Revert(self):
717 """Reverts the kernel partition update."""
718 # There is nothing to do for reverting kernel partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800719
720
721class RootfsUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600722 """A class to update the root partition on a Chromium OS device."""
Amin Hassani75c5f942021-02-20 23:56:53 -0800723
Alex Klein1699fab2022-09-08 08:46:06 -0600724 def __init__(self, current_root: str, *args):
725 """Initializes the class.
Amin Hassani75c5f942021-02-20 23:56:53 -0800726
Alex Klein1699fab2022-09-08 08:46:06 -0600727 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600728 current_root: The current root device path.
729 *args: See PartitionUpdaterBase
Alex Klein1699fab2022-09-08 08:46:06 -0600730 """
731 super().__init__(*args)
Amin Hassani75c5f942021-02-20 23:56:53 -0800732
Alex Klein1699fab2022-09-08 08:46:06 -0600733 self._current_root = current_root
734 self._ran_postinst = False
Amin Hassani75c5f942021-02-20 23:56:53 -0800735
Alex Klein1699fab2022-09-08 08:46:06 -0600736 def _GetPartitionName(self):
737 """See RawPartitionUpdater._GetPartitionName()."""
738 return constants.PART_ROOT_A
Amin Hassani75c5f942021-02-20 23:56:53 -0800739
Alex Klein1699fab2022-09-08 08:46:06 -0600740 def _GetRemotePartitionName(self):
741 """See RawPartitionUpdater._GetRemotePartitionName()."""
742 return constants.QUICK_PROVISION_PAYLOAD_ROOTFS
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800743
Alex Klein1699fab2022-09-08 08:46:06 -0600744 def _Run(self):
745 """The function that does the job of rootfs partition update."""
746 with ProgressWatcher(self._device, self._target):
747 super()._Run()
Amin Hassani75c5f942021-02-20 23:56:53 -0800748
Alex Klein1699fab2022-09-08 08:46:06 -0600749 self._RunPostInst()
Amin Hassani75c5f942021-02-20 23:56:53 -0800750
Alex Klein1699fab2022-09-08 08:46:06 -0600751 def _OptimizePartLocation(self, offset: int, length: int):
752 """Optimizes the size of the root partition of the image.
Amin Hassani75c5f942021-02-20 23:56:53 -0800753
Alex Klein1699fab2022-09-08 08:46:06 -0600754 Normally the file system does not occupy the entire partition. Furthermore
755 we don't need the verity hash tree at the end of the root file system
756 because postinst will recreate it. This function reads the (approximate)
757 superblock of the ext4 partition and extracts the actual file system size in
758 the root partition.
759 """
760 superblock_size = 4096 * 2
761 with open(self._image, "rb") as r:
762 r.seek(offset)
763 with tempfile.NamedTemporaryFile(delete=False) as fp:
764 fp.write(r.read(superblock_size))
765 fp.close()
766 return offset, partition_lib.Ext2FileSystemSize(fp.name)
Amin Hassani75c5f942021-02-20 23:56:53 -0800767
Alex Klein1699fab2022-09-08 08:46:06 -0600768 def _RunPostInst(self, on_target: bool = True):
769 """Runs the postinst process in the root partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800770
Alex Klein1699fab2022-09-08 08:46:06 -0600771 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600772 on_target: If true the postinst is run on the target (inactive)
773 partition. This is used when doing normal updates. If false, the
774 postinst is run on the current (active) partition. This is used
775 when reverting an update.
Alex Klein1699fab2022-09-08 08:46:06 -0600776 """
777 try:
778 postinst_dir = "/"
779 partition = self._current_root
780 if on_target:
781 postinst_dir = self._device.run(
782 ["mktemp", "-d", "-p", self._device.work_dir],
783 capture_output=True,
784 ).stdout.strip()
785 self._device.run(
786 ["mount", "-o", "ro", self._target, postinst_dir]
787 )
788 partition = self._target
Amin Hassani75c5f942021-02-20 23:56:53 -0800789
Alex Klein1699fab2022-09-08 08:46:06 -0600790 self._ran_postinst = True
791 postinst = os.path.join(postinst_dir, "postinst")
792 result = self._device.run(
793 [postinst, partition], capture_output=True
794 )
Amin Hassani75c5f942021-02-20 23:56:53 -0800795
Alex Klein1699fab2022-09-08 08:46:06 -0600796 logging.debug(
797 "Postinst result on %s: \n%s", postinst, result.stdout
798 )
799 # DeviceImagerOperation will look for this log.
800 logging.info("Postinstall completed.")
801 finally:
802 if on_target:
803 self._device.run(["umount", postinst_dir])
Amin Hassani75c5f942021-02-20 23:56:53 -0800804
Alex Klein1699fab2022-09-08 08:46:06 -0600805 def Revert(self):
806 """Reverts the root update install."""
807 logging.info("Reverting the rootfs partition update.")
808 if self._ran_postinst:
809 # We don't have to do anything for revert if we haven't changed the kernel
810 # priorities yet.
811 self._RunPostInst(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800812
813
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000814class MiniOSUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600815 """A class to update the miniOS partition on a Chromium OS device."""
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000816
Alex Klein1699fab2022-09-08 08:46:06 -0600817 def __init__(self, *args):
818 """Initializes the class.
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000819
Alex Klein1699fab2022-09-08 08:46:06 -0600820 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600821 *args: See PartitionUpdaterBase
Alex Klein1699fab2022-09-08 08:46:06 -0600822 """
823 super().__init__(*args)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000824
Alex Klein1699fab2022-09-08 08:46:06 -0600825 self._ran_postinst = False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000826
Alex Klein1699fab2022-09-08 08:46:06 -0600827 def _GetPartitionName(self):
828 """See RawPartitionUpdater._GetPartitionName()."""
829 return constants.PART_MINIOS_A
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000830
Alex Klein1699fab2022-09-08 08:46:06 -0600831 def _GetRemotePartitionName(self):
832 """See RawPartitionUpdater._GetRemotePartitionName()."""
833 return constants.QUICK_PROVISION_PAYLOAD_MINIOS
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000834
Alex Klein1699fab2022-09-08 08:46:06 -0600835 def _Run(self):
836 """The function that does the job of rootfs partition update."""
837 if self._image_type == ImageType.FULL:
838 if self._MiniOSPartitionsExistInImage():
839 logging.info("Updating miniOS partition from local.")
840 super()._Run()
841 else:
842 logging.warning(
843 "Not updating miniOS partition as it does not exist."
844 )
845 return
846 elif self._image_type == ImageType.REMOTE_DIRECTORY:
847 if not gs.GSContext().Exists(
848 os.path.join(
849 self._image, constants.QUICK_PROVISION_PAYLOAD_MINIOS
850 )
851 ):
852 logging.warning("Not updating miniOS, missing remote files.")
853 return
854 elif not self._MiniOSPartitionsExist():
855 logging.warning("Not updating miniOS, missing partitions.")
856 return
857 else:
858 logging.info("Updating miniOS partition from remote.")
859 super()._Run()
860 else:
861 # Let super() handle this error.
862 super()._Run()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000863
Alex Klein1699fab2022-09-08 08:46:06 -0600864 self._RunPostInstall()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000865
Alex Klein1699fab2022-09-08 08:46:06 -0600866 def _RunPostInstall(self):
867 """The function will change the priority of the miniOS partitions."""
868 self._FlipMiniOSPriority()
869 self._ran_postinst = True
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000870
Alex Klein1699fab2022-09-08 08:46:06 -0600871 def Revert(self):
872 """Reverts the miniOS partition update."""
873 if self._ran_postinst:
874 self._FlipMiniOSPriority()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000875
Alex Klein1699fab2022-09-08 08:46:06 -0600876 def _GetMiniOSPriority(self):
877 return self._device.run(
878 ["crossystem", constants.MINIOS_PRIORITY]
879 ).stdout
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000880
Alex Klein1699fab2022-09-08 08:46:06 -0600881 def _SetMiniOSPriority(self, priority: str):
882 self._device.run(
883 ["crossystem", f"{constants.MINIOS_PRIORITY}={priority}"]
884 )
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000885
Alex Klein1699fab2022-09-08 08:46:06 -0600886 def _FlipMiniOSPriority(self):
887 inactive_minios_priority = (
888 "B" if self._GetMiniOSPriority() == "A" else "A"
889 )
890 logging.info("Setting miniOS priority to %s", inactive_minios_priority)
891 self._SetMiniOSPriority(inactive_minios_priority)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000892
Alex Klein1699fab2022-09-08 08:46:06 -0600893 def _MiniOSPartitionsExistInImage(self):
894 """Checks if miniOS partition exists in the image."""
895 d = cgpt.Disk.FromImage(self._image)
896 try:
897 d.GetPartitionByTypeGuid(cgpt.MINIOS_TYPE_GUID)
898 return True
899 except KeyError:
900 return False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000901
Alex Klein1699fab2022-09-08 08:46:06 -0600902 def _MiniOSPartitionsExist(self):
903 """Checks if the device has miniOS partitions."""
904 run = lambda x: self._device.run(x).stdout.strip()
905 device_drive = run(["rootdev", "-s", "-d"])
906 cmd = ["cgpt", "show", "-t", device_drive, "-i"]
907 return all(
908 (run(cmd + [p]) == cgpt.MINIOS_TYPE_GUID) for p in ("9", "10")
909 )
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700910
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000911
Amin Hassani74403082021-02-22 11:40:09 -0800912class StatefulPayloadGenerator(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600913 """A class for generating a stateful update payload in a separate thread."""
Amin Hassani74403082021-02-22 11:40:09 -0800914
Alex Klein1699fab2022-09-08 08:46:06 -0600915 def __init__(self, image: str):
916 """Initializes that class.
Amin Hassani74403082021-02-22 11:40:09 -0800917
Alex Klein1699fab2022-09-08 08:46:06 -0600918 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600919 image: The path to a local Chromium OS image.
Alex Klein1699fab2022-09-08 08:46:06 -0600920 """
921 super().__init__()
922 self._image = image
923
924 def run(self):
925 """Generates the stateful update and writes it into the output pipe."""
926 try:
927 paygen_stateful_payload_lib.GenerateStatefulPayload(
928 self._image, self._Source()
929 )
930 finally:
931 self._CloseSource()
Amin Hassani74403082021-02-22 11:40:09 -0800932
933
934class StatefulUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600935 """A class to update the stateful partition on a device."""
Amin Hassani74403082021-02-22 11:40:09 -0800936
Alex Klein1699fab2022-09-08 08:46:06 -0600937 def __init__(self, clobber_stateful: bool, *args):
938 """Initializes the class
Amin Hassani74403082021-02-22 11:40:09 -0800939
Alex Klein1699fab2022-09-08 08:46:06 -0600940 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600941 clobber_stateful: Whether to clobber the stateful or not.
942 *args: Look at PartitionUpdaterBase.
Alex Klein1699fab2022-09-08 08:46:06 -0600943 """
944 super().__init__(*args)
945 self._clobber_stateful = clobber_stateful
Amin Hassani74403082021-02-22 11:40:09 -0800946
Alex Klein1699fab2022-09-08 08:46:06 -0600947 def _Run(self):
948 """Reads/Downloads the stateful updates and writes it into the device."""
949 if self._image_type == ImageType.FULL:
950 generator_cls = StatefulPayloadGenerator
951 elif self._image_type == ImageType.REMOTE_DIRECTORY:
952 generator_cls = GsFileCopier
953 self._image = os.path.join(
954 self._image, paygen_stateful_payload_lib.STATEFUL_FILE
955 )
956 else:
957 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassani74403082021-02-22 11:40:09 -0800958
Alex Klein1699fab2022-09-08 08:46:06 -0600959 with generator_cls(self._image) as generator:
960 try:
961 updater = stateful_updater.StatefulUpdater(self._device)
962 updater.Update(
963 generator.Target(),
964 is_payload_on_device=False,
965 update_type=(
966 stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER
967 if self._clobber_stateful
968 else None
969 ),
970 )
971 finally:
972 generator.CloseTarget()
973
974 def Revert(self):
975 """Reverts the stateful partition update."""
976 logging.info("Reverting the stateful update.")
977 stateful_updater.StatefulUpdater(self._device).Reset()
Amin Hassani55970562021-02-22 20:49:13 -0800978
979
980class ProgressWatcher(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600981 """A class used for watching the progress of rootfs update."""
Amin Hassani55970562021-02-22 20:49:13 -0800982
Alex Klein1699fab2022-09-08 08:46:06 -0600983 def __init__(self, device, target_root: str):
984 """Initializes the class.
Amin Hassani55970562021-02-22 20:49:13 -0800985
Alex Klein1699fab2022-09-08 08:46:06 -0600986 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600987 device: The ChromiumOSDevice to be updated.
988 target_root: The target root partition to monitor the progress of.
Alex Klein1699fab2022-09-08 08:46:06 -0600989 """
990 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -0800991
Alex Klein1699fab2022-09-08 08:46:06 -0600992 self._device = device
993 self._target_root = target_root
994 self._exit = False
Amin Hassani55970562021-02-22 20:49:13 -0800995
Alex Klein1699fab2022-09-08 08:46:06 -0600996 def __enter__(self):
997 """Starts the thread."""
998 self.start()
999 return self
Amin Hassani55970562021-02-22 20:49:13 -08001000
Alex Klein1699fab2022-09-08 08:46:06 -06001001 def __exit__(self, *args, **kwargs):
1002 """Exists the thread."""
1003 self._exit = True
1004 self.join()
Amin Hassani55970562021-02-22 20:49:13 -08001005
Alex Klein1699fab2022-09-08 08:46:06 -06001006 def _ShouldExit(self):
1007 return self._exit
Amin Hassani55970562021-02-22 20:49:13 -08001008
Alex Klein1699fab2022-09-08 08:46:06 -06001009 def run(self):
1010 """Monitors the progress of the target root partitions' update.
Amin Hassani55970562021-02-22 20:49:13 -08001011
Alex Klein1699fab2022-09-08 08:46:06 -06001012 This is done by periodically, reading the fd position of the process that is
1013 writing into the target partition and reporting it back. Then the position
1014 is divided by the size of the block device to report an approximate
1015 progress.
1016 """
1017 cmd = ["blockdev", "--getsize64", self._target_root]
Amin Hassani55970562021-02-22 20:49:13 -08001018 output = self._device.run(cmd, capture_output=True).stdout.strip()
Alex Klein1699fab2022-09-08 08:46:06 -06001019 if output is None:
1020 raise Error(f"Cannot get the block device size from {output}.")
1021 dev_size = int(output)
1022
1023 # Using lsof to find out which process is writing to the target rootfs.
1024 cmd = ["lsof", "-t", self._target_root]
1025 while not self._ShouldExit():
1026 try:
1027 pid = self._device.run(cmd, capture_output=True).stdout.strip()
1028 if pid:
1029 break
1030 except cros_build_lib.RunCommandError:
1031 continue
1032 finally:
1033 time.sleep(1)
1034
1035 # Now that we know which process is writing to it, we can look the fdinfo of
1036 # stdout of that process to get its offset. We're assuming there will be no
1037 # seek, which is correct.
1038 cmd = ["cat", f"/proc/{pid}/fdinfo/1"]
1039 while not self._ShouldExit():
1040 try:
1041 output = self._device.run(
1042 cmd, capture_output=True
1043 ).stdout.strip()
1044 m = re.search(r"^pos:\s*(\d+)$", output, flags=re.M)
1045 if m:
1046 offset = int(m.group(1))
1047 # DeviceImagerOperation will look for this log.
1048 logging.info("RootFS progress: %f", offset / dev_size)
1049 except cros_build_lib.RunCommandError:
1050 continue
1051 finally:
1052 time.sleep(1)
Amin Hassani55970562021-02-22 20:49:13 -08001053
1054
1055class DeviceImagerOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -06001056 """A class to provide a progress bar for DeviceImager operation."""
Amin Hassani55970562021-02-22 20:49:13 -08001057
Alex Klein1699fab2022-09-08 08:46:06 -06001058 def __init__(self):
1059 """Initializes the class."""
1060 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -08001061
Alex Klein1699fab2022-09-08 08:46:06 -06001062 self._progress = 0.0
Amin Hassani55970562021-02-22 20:49:13 -08001063
Alex Klein1699fab2022-09-08 08:46:06 -06001064 def ParseOutput(self, output=None):
1065 """Override function to parse the output and provide progress.
Amin Hassani55970562021-02-22 20:49:13 -08001066
Alex Klein1699fab2022-09-08 08:46:06 -06001067 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001068 output: The stderr or stdout.
Alex Klein1699fab2022-09-08 08:46:06 -06001069 """
1070 output = self._stdout.read()
1071 match = re.findall(r"RootFS progress: (\d+(?:\.\d+)?)", output)
1072 if match:
1073 progress = float(match[0])
1074 self._progress = max(self._progress, progress)
Amin Hassani55970562021-02-22 20:49:13 -08001075
Alex Klein1699fab2022-09-08 08:46:06 -06001076 # If postinstall completes, move half of the remaining progress.
1077 if re.findall(r"Postinstall completed", output):
1078 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001079
Alex Klein1699fab2022-09-08 08:46:06 -06001080 # While waiting for reboot, each time, move half of the remaining progress.
1081 if re.findall(r"Unable to get new boot_id", output):
1082 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001083
Alex Klein1699fab2022-09-08 08:46:06 -06001084 if re.findall(r"DeviceImager completed.", output):
1085 self._progress = 1.0
Amin Hassani55970562021-02-22 20:49:13 -08001086
Alex Klein1699fab2022-09-08 08:46:06 -06001087 self.ProgressBar(self._progress)