blob: 1bbe1f7a53ac3b72559821ab35c2378068168c3b [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
Alex Klein1699fab2022-09-08 08:46:06 -0600155 if not self._no_reboot:
156 self._Reboot()
157 self._VerifyBootExpectations()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800158
Alex Klein1699fab2022-09-08 08:46:06 -0600159 if self._disable_verification:
160 self._device.DisableRootfsVerification()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800161
Alex Klein1699fab2022-09-08 08:46:06 -0600162 def _LocateImage(self):
163 """Locates the path to the final image(s) that need to be installed.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800164
Alex Klein1699fab2022-09-08 08:46:06 -0600165 If the paths is local, the image should be the Chromium OS GPT image
166 (e.g. chromiumos_test_image.bin). If the path is remote, it should be the
167 remote directory where we can find the quick-provision and stateful update
168 files (e.g. gs://chromeos-image-archive/eve-release/R90-x.x.x).
Amin Hassanicf8f0042021-03-12 10:42:13 -0800169
Alex Klein1699fab2022-09-08 08:46:06 -0600170 NOTE: At this point there is no caching involved. Hence we always download
171 the partition payloads or extract them from the Chromium OS image.
172 """
173 if os.path.isfile(self._image):
174 self._image_type = ImageType.FULL
175 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800176
Alex Klein1699fab2022-09-08 08:46:06 -0600177 # TODO(b/172212406): We could potentially also allow this by searching
178 # through the directory to see whether we have quick-provision and stateful
179 # payloads. This only makes sense when a user has their workstation at home
180 # and doesn't want to incur the bandwidth cost of downloading the same
181 # image multiple times. For that, they can simply download the GPT image
182 # image first and flash that instead.
183 if os.path.isdir(self._image):
184 raise ValueError(
185 f"{self._image}: input must be a disk image, not a directory."
186 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800187
Alex Klein1699fab2022-09-08 08:46:06 -0600188 if gs.PathIsGs(self._image):
189 # TODO(b/172212406): Check whether it is a directory. If it wasn't a
190 # directory download the image into some temp location and use it instead.
191 self._image_type = ImageType.REMOTE_DIRECTORY
192 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800193
Alex Klein1699fab2022-09-08 08:46:06 -0600194 # Assuming it is an xBuddy path.
195 board = cros_build_lib.GetBoard(
196 device_board=self._device.board or flash.GetDefaultBoard(),
197 override_board=self._board,
198 force=True,
199 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800200
Alex Klein1699fab2022-09-08 08:46:06 -0600201 xb = xbuddy.XBuddy(board=board, version=self._version)
202 build_id, local_file = xb.Translate([self._image])
203 if build_id is None:
204 raise Error(f"{self._image}: unable to find matching xBuddy path.")
205 logging.info("XBuddy path translated to build ID %s", build_id)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800206
Alex Klein1699fab2022-09-08 08:46:06 -0600207 if local_file:
208 self._image = local_file
209 self._image_type = ImageType.FULL
210 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800211
Alex Klein1699fab2022-09-08 08:46:06 -0600212 self._image = f"{devserver_constants.GS_IMAGE_DIR}/{build_id}"
213 self._image_type = ImageType.REMOTE_DIRECTORY
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800214
Alex Klein1699fab2022-09-08 08:46:06 -0600215 def _SplitDevPath(self, path: str) -> Tuple[str, int]:
216 """Splits the given /dev/x path into prefix and the dev number.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800217
Alex Klein1699fab2022-09-08 08:46:06 -0600218 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600219 path: The path to a block dev device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800220
Alex Klein1699fab2022-09-08 08:46:06 -0600221 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600222 A tuple of representing the prefix and the index of the dev path.
223 e.g.: '/dev/mmcblk0p1' -> ['/dev/mmcblk0p', 1]
Alex Klein1699fab2022-09-08 08:46:06 -0600224 """
225 match = re.search(r"(.*)([0-9]+)$", path)
226 if match is None:
227 raise Error(f"{path}: Could not parse root dev path.")
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000228
Alex Klein1699fab2022-09-08 08:46:06 -0600229 return match.group(1), int(match.group(2))
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000230
Alex Klein1699fab2022-09-08 08:46:06 -0600231 def _GetKernelState(self, root_num: int) -> Tuple[Dict, Dict]:
232 """Returns the kernel state.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800233
Alex Klein1699fab2022-09-08 08:46:06 -0600234 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600235 A tuple of two dictionaries: The current active kernel state and the
236 inactive kernel state. (Look at A and B constants in this class.)
Alex Klein1699fab2022-09-08 08:46:06 -0600237 """
238 if root_num == self.A[Partition.ROOTFS]:
239 return self.A, self.B
240 elif root_num == self.B[Partition.ROOTFS]:
241 return self.B, self.A
242 else:
243 raise Error(f"Invalid root partition number {root_num}")
Amin Hassanid684e982021-02-26 11:10:58 -0800244
Alex Klein1699fab2022-09-08 08:46:06 -0600245 def _GetMiniOSState(self, minios_num: int) -> Tuple[Dict, Dict]:
246 """Returns the miniOS state.
Amin Hassani75c5f942021-02-20 23:56:53 -0800247
Alex Klein1699fab2022-09-08 08:46:06 -0600248 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600249 A tuple of dictionaries: The current active miniOS state and the
250 inactive miniOS state.
Alex Klein1699fab2022-09-08 08:46:06 -0600251 """
252 if minios_num == self.MINIOS_A[Partition.MINIOS]:
253 return self.MINIOS_A, self.MINIOS_B
254 elif minios_num == self.MINIOS_B[Partition.MINIOS]:
255 return self.MINIOS_B, self.MINIOS_A
256 else:
257 raise Error(f"Invalid minios partition number {minios_num}")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800258
Alex Klein1699fab2022-09-08 08:46:06 -0600259 def _InstallPartitions(self):
260 """The main method that installs the partitions of a Chrome OS device.
Amin Hassani74403082021-02-22 11:40:09 -0800261
Alex Klein1699fab2022-09-08 08:46:06 -0600262 It uses parallelism to install the partitions as fast as possible.
263 """
264 prefix, root_num = self._SplitDevPath(self._device.root_dev)
265 active_state, self._inactive_state = self._GetKernelState(root_num)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000266
Alex Klein1699fab2022-09-08 08:46:06 -0600267 updaters = []
268 if not self._no_rootfs_update:
269 current_root = prefix + str(active_state[Partition.ROOTFS])
270 target_root = prefix + str(self._inactive_state[Partition.ROOTFS])
271 updaters.append(
272 RootfsUpdater(
273 current_root,
274 self._device,
275 self._image,
276 self._image_type,
277 target_root,
278 )
279 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800280
Alex Klein1699fab2022-09-08 08:46:06 -0600281 target_kernel = prefix + str(self._inactive_state[Partition.KERNEL])
282 updaters.append(
283 KernelUpdater(
284 self._device, self._image, self._image_type, target_kernel
285 )
286 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800287
Alex Klein1699fab2022-09-08 08:46:06 -0600288 if not self._no_stateful_update:
289 updaters.append(
290 StatefulUpdater(
291 self._clobber_stateful,
292 self._device,
293 self._image,
294 self._image_type,
295 None,
296 )
297 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800298
Alex Klein1699fab2022-09-08 08:46:06 -0600299 if not self._no_minios_update:
300 minios_priority = self._device.run(
301 ["crossystem", constants.MINIOS_PRIORITY]
302 ).stdout
303 if minios_priority not in ["A", "B"]:
304 logging.warning(
305 "Skipping miniOS flash due to missing priority."
306 )
307 else:
308 # Reference disk_layout_v3 for partition numbering.
309 _, inactive_minios_state = self._GetMiniOSState(
310 9 if minios_priority == "A" else 10
311 )
312 target_minios = prefix + str(
313 inactive_minios_state[Partition.MINIOS]
314 )
315 minios_updater = MiniOSUpdater(
316 self._device, self._image, self._image_type, target_minios
317 )
318 updaters.append(minios_updater)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800319
Alex Klein1699fab2022-09-08 08:46:06 -0600320 # Retry the partitions updates that failed, in case a transient error (like
321 # SSH drop, etc) caused the error.
322 num_retries = 1
323 try:
324 retry_util.RetryException(
325 Error,
326 num_retries,
327 parallel.RunParallelSteps,
328 (x.Run for x in updaters if not x.IsFinished()),
329 halt_on_error=True,
330 )
331 except Exception:
332 # If one of the partitions failed to be installed, revert all partitions.
333 parallel.RunParallelSteps(x.Revert for x in updaters)
334 raise
335
336 def _Reboot(self):
337 """Reboots the device."""
338 try:
339 self._device.Reboot(timeout_sec=300)
340 except remote_access.RebootError:
341 raise Error(
342 "Could not recover from reboot. Once example reason"
343 " could be the image provided was a non-test image"
344 " or the system failed to boot after the update."
345 )
346 except Exception as e:
347 raise Error(f"Failed to reboot to the device with error: {e}")
348
349 def _VerifyBootExpectations(self):
350 """Verify that we fully booted into the expected kernel state."""
351 # Discover the newly active kernel.
352 _, root_num = self._SplitDevPath(self._device.root_dev)
353 active_state, _ = self._GetKernelState(root_num)
354
355 # If this happens, we should rollback.
356 if active_state != self._inactive_state:
357 raise Error("The expected kernel state after update is invalid.")
358
359 logging.info("Verified boot expectations.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800360
361
362class ReaderBase(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600363 """The base class for reading different inputs and writing into output.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800364
Alex Klein1699fab2022-09-08 08:46:06 -0600365 This class extends threading.Thread, so it will be run on its own thread. Also
366 it can be used as a context manager. Internally, it opens necessary files for
367 writing to and reading from. This class cannot be instantiated, it needs to be
368 sub-classed first to provide necessary function implementations.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800369 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800370
Alex Klein1699fab2022-09-08 08:46:06 -0600371 def __init__(self, use_named_pipes: bool = False):
372 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800373
Alex Klein1699fab2022-09-08 08:46:06 -0600374 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600375 use_named_pipes: Whether to use a named pipe or anonymous file
Alex Klein1699fab2022-09-08 08:46:06 -0600376 descriptors.
377 """
378 super().__init__()
379 self._use_named_pipes = use_named_pipes
380 self._pipe_target = None
381 self._pipe_source = None
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800382
Alex Klein1699fab2022-09-08 08:46:06 -0600383 def __del__(self):
384 """Destructor.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800385
Alex Klein1699fab2022-09-08 08:46:06 -0600386 Make sure to clean up any named pipes we might have created.
387 """
388 if self._use_named_pipes:
389 osutils.SafeUnlink(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800390
Alex Klein1699fab2022-09-08 08:46:06 -0600391 def __enter__(self):
392 """Enters the context manager"""
393 if self._use_named_pipes:
394 # There is no need for the temp file, we only need its path. So the named
395 # pipe is created after this temp file is deleted.
396 with tempfile.NamedTemporaryFile(
397 prefix="chromite-device-imager"
398 ) as fp:
399 self._pipe_target = self._pipe_source = fp.name
400 os.mkfifo(self._pipe_target)
401 else:
402 self._pipe_target, self._pipe_source = os.pipe()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800403
Alex Klein1699fab2022-09-08 08:46:06 -0600404 self.start()
405 return self
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800406
Alex Klein1699fab2022-09-08 08:46:06 -0600407 def __exit__(self, *args, **kwargs):
408 """Exits the context manager."""
409 self.join()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800410
Alex Klein1699fab2022-09-08 08:46:06 -0600411 def _Source(self):
412 """Returns the source pipe to write data into.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800413
Alex Klein1699fab2022-09-08 08:46:06 -0600414 Sub-classes can use this function to determine where to write their data
415 into.
416 """
417 return self._pipe_source
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800418
Alex Klein1699fab2022-09-08 08:46:06 -0600419 def _CloseSource(self):
420 """Closes the source pipe.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800421
Alex Klein1699fab2022-09-08 08:46:06 -0600422 Sub-classes should use this function to close the pipe after they are done
423 writing into it. Failure to do so may result reader of the data to hang
424 indefinitely.
425 """
426 if not self._use_named_pipes:
427 os.close(self._pipe_source)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800428
Alex Klein1699fab2022-09-08 08:46:06 -0600429 def Target(self):
430 """Returns the target pipe to read data from.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800431
Alex Klein1699fab2022-09-08 08:46:06 -0600432 Users of this class can use this path to read data from.
433 """
434 return self._pipe_target
435
436 def CloseTarget(self):
437 """Closes the target pipe.
438
439 Users of this class should use this function to close the pipe after they
440 are done reading from it.
441 """
442 if self._use_named_pipes:
443 os.remove(self._pipe_target)
444 else:
445 os.close(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800446
447
448class PartialFileReader(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600449 """A class to read specific offset and length from a file and compress it.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800450
Alex Klein1699fab2022-09-08 08:46:06 -0600451 This class can be used to read from specific location and length in a file
452 (e.g. A partition in a GPT image). Then it compresses the input and writes it
453 out (to a pipe). Look at the base class for more information.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800454 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800455
Alex Klein1699fab2022-09-08 08:46:06 -0600456 # The offset of different partitions in a Chromium OS image does not always
457 # align to larger values like 4096. It seems that 512 is the maximum value to
458 # be divisible by partition offsets. This size should not be increased just
459 # for 'performance reasons'. Since we are doing everything in parallel, in
460 # practice there is not much difference between this and larger block sizes as
461 # parallelism hides the possible extra latency provided by smaller block
462 # sizes.
463 _BLOCK_SIZE = 512
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800464
Alex Klein1699fab2022-09-08 08:46:06 -0600465 def __init__(
466 self,
467 image: str,
468 offset: int,
469 length: int,
470 compression_command: List[str],
471 ):
472 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800473
Alex Klein1699fab2022-09-08 08:46:06 -0600474 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600475 image: The path to an image (local or remote directory).
476 offset: The offset (in bytes) to read from the image.
477 length: The length (in bytes) to read from the image.
478 compression_command: The command to compress transferred bytes.
Alex Klein1699fab2022-09-08 08:46:06 -0600479 """
480 super().__init__()
481
482 self._image = image
483 self._offset = offset
484 self._length = length
485 self._compression_command = compression_command
486
487 def run(self):
488 """Runs the reading and compression."""
Mike Frysinger906119e2022-12-27 18:10:23 -0500489 data = osutils.ReadFile(
490 self._image, mode="rb", size=self._length, seek=self._offset
491 )
Alex Klein1699fab2022-09-08 08:46:06 -0600492 try:
Mike Frysinger906119e2022-12-27 18:10:23 -0500493 cros_build_lib.run(
494 self._compression_command, input=data, stdout=self._Source()
495 )
Alex Klein1699fab2022-09-08 08:46:06 -0600496 finally:
497 self._CloseSource()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800498
499
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800500class GsFileCopier(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600501 """A class for downloading gzip compressed file from GS bucket into a pipe."""
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800502
Alex Klein1699fab2022-09-08 08:46:06 -0600503 def __init__(self, image: str):
504 """Initializes the class.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800505
Alex Klein1699fab2022-09-08 08:46:06 -0600506 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600507 image: The path to an image (local or remote directory).
Alex Klein1699fab2022-09-08 08:46:06 -0600508 """
509 super().__init__(use_named_pipes=True)
510 self._image = image
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800511
Alex Klein1699fab2022-09-08 08:46:06 -0600512 def run(self):
513 """Runs the download and write into the output pipe."""
514 try:
515 gs.GSContext().Copy(self._image, self._Source())
516 finally:
517 self._CloseSource()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800518
519
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800520class PartitionUpdaterBase(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600521 """A base abstract class to use for installing an image into a partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800522
Alex Klein1699fab2022-09-08 08:46:06 -0600523 Sub-classes should implement the abstract methods to provide the core
524 functionality.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800525 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800526
Alex Klein1699fab2022-09-08 08:46:06 -0600527 def __init__(self, device, image: str, image_type, target: str):
528 """Initializes this base class with values that most sub-classes will need.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800529
Alex Klein1699fab2022-09-08 08:46:06 -0600530 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600531 device: The ChromiumOSDevice to be updated.
532 image: The target image path for the partition update.
533 image_type: The type of the image (ImageType).
534 target: The target path (e.g. block dev) to install the update.
Alex Klein1699fab2022-09-08 08:46:06 -0600535 """
536 self._device = device
537 self._image = image
538 self._image_type = image_type
539 self._target = target
540 self._finished = False
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800541
Alex Klein1699fab2022-09-08 08:46:06 -0600542 def Run(self):
543 """The main function that does the partition update job."""
544 with timer.Timer() as t:
545 try:
546 self._Run()
547 finally:
548 self._finished = True
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800549
Alex Klein1699fab2022-09-08 08:46:06 -0600550 logging.debug("Completed %s in %s", self.__class__.__name__, t)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800551
Alex Klein1699fab2022-09-08 08:46:06 -0600552 @abc.abstractmethod
553 def _Run(self):
554 """The method that need to be implemented by sub-classes."""
555 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800556
Alex Klein1699fab2022-09-08 08:46:06 -0600557 def IsFinished(self):
558 """Returns whether the partition update has been successful."""
559 return self._finished
560
561 @abc.abstractmethod
562 def Revert(self):
563 """Reverts the partition update.
564
565 Sub-classes need to implement this function to provide revert capability.
566 """
567 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800568
569
570class RawPartitionUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600571 """A class to update a raw partition on a Chromium OS device."""
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800572
Alex Klein1699fab2022-09-08 08:46:06 -0600573 def _Run(self):
574 """The function that does the job of kernel partition update."""
575 if self._image_type == ImageType.FULL:
576 self._CopyPartitionFromImage(self._GetPartitionName())
577 elif self._image_type == ImageType.REMOTE_DIRECTORY:
578 self._RedirectPartition(self._GetRemotePartitionName())
579 else:
580 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800581
Alex Klein1699fab2022-09-08 08:46:06 -0600582 def _GetPartitionName(self):
583 """Returns the name of the partition in a Chromium OS GPT layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800584
Alex Klein1699fab2022-09-08 08:46:06 -0600585 Subclasses should override this function to return correct name.
586 """
587 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800588
Alex Klein1699fab2022-09-08 08:46:06 -0600589 def _CopyPartitionFromImage(self, part_name: str):
590 """Updates the device's partition from a local Chromium OS image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800591
Alex Klein1699fab2022-09-08 08:46:06 -0600592 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600593 part_name: The name of the partition in the source image that needs
594 to be extracted.
Alex Klein1699fab2022-09-08 08:46:06 -0600595 """
596 offset, length = self._GetPartLocation(part_name)
597 offset, length = self._OptimizePartLocation(offset, length)
598 compressor, decompressor = self._GetCompressionAndDecompression()
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900599
Alex Klein1699fab2022-09-08 08:46:06 -0600600 with PartialFileReader(
601 self._image, offset, length, compressor
602 ) as generator:
603 try:
604 self._WriteToTarget(generator.Target(), decompressor)
605 finally:
606 generator.CloseTarget()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800607
Alex Klein1699fab2022-09-08 08:46:06 -0600608 def _GetCompressionAndDecompression(self) -> Tuple[List[str], List[str]]:
609 """Returns compression / decompression commands."""
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900610
Alex Klein1699fab2022-09-08 08:46:06 -0600611 return (
Mike Frysinger66306012022-04-22 15:23:13 -0400612 [
613 cros_build_lib.FindCompressor(
614 cros_build_lib.CompressionType.GZIP
615 )
616 ],
617 self._device.GetDecompressor(cros_build_lib.CompressionType.GZIP),
Alex Klein1699fab2022-09-08 08:46:06 -0600618 )
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900619
Alex Klein1699fab2022-09-08 08:46:06 -0600620 def _WriteToTarget(
621 self, source: Union[int, BytesIO], decompress_command: List[str]
622 ) -> None:
623 """Writes bytes source to the target device on DUT.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800624
Alex Klein1699fab2022-09-08 08:46:06 -0600625 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600626 A string command to run on a device to read data from stdin,
627 uncompress it and write it to the target partition.
Alex Klein1699fab2022-09-08 08:46:06 -0600628 """
629 # Using oflag=direct to tell the OS not to cache the writes (faster).
630 cmd = " ".join(
631 [
632 *decompress_command,
633 "|",
634 "dd",
635 "bs=1M",
636 "oflag=direct",
637 f"of={self._target}",
638 ]
639 )
640 self._device.run(cmd, input=source, shell=True)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800641
Alex Klein1699fab2022-09-08 08:46:06 -0600642 def _GetPartLocation(self, part_name: str):
643 """Extracts the location and size of the raw partition from the image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800644
Alex Klein1699fab2022-09-08 08:46:06 -0600645 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600646 part_name: The name of the partition in the source image that needs
647 to be extracted.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800648
Alex Klein1699fab2022-09-08 08:46:06 -0600649 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600650 A tuple of offset and length (in bytes) from the image.
Alex Klein1699fab2022-09-08 08:46:06 -0600651 """
652 try:
653 parts = image_lib.GetImageDiskPartitionInfo(self._image)
654 part_info = [p for p in parts if p.name == part_name][0]
655 except IndexError:
656 raise Error(f"No partition named {part_name} found.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800657
Alex Klein1699fab2022-09-08 08:46:06 -0600658 return int(part_info.start), int(part_info.size)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800659
Alex Klein1699fab2022-09-08 08:46:06 -0600660 def _GetRemotePartitionName(self):
661 """Returns the name of the quick-provision partition file.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800662
Alex Klein1699fab2022-09-08 08:46:06 -0600663 Subclasses should override this function to return correct name.
664 """
665 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800666
Alex Klein1699fab2022-09-08 08:46:06 -0600667 def _OptimizePartLocation(self, offset: int, length: int):
668 """Optimizes the offset and length of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800669
Alex Klein1699fab2022-09-08 08:46:06 -0600670 Subclasses can override this to provide better offset/length than what is
671 defined in the PGT partition layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800672
Alex Klein1699fab2022-09-08 08:46:06 -0600673 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600674 offset: The offset (in bytes) of the partition in the image.
675 length: The length (in bytes) of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800676
Alex Klein1699fab2022-09-08 08:46:06 -0600677 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600678 A tuple of offset and length (in bytes) from the image.
Alex Klein1699fab2022-09-08 08:46:06 -0600679 """
680 return offset, length
Amin Hassanid684e982021-02-26 11:10:58 -0800681
Alex Klein1699fab2022-09-08 08:46:06 -0600682 def _RedirectPartition(self, file_name: str):
683 """Downloads the partition from a remote path and writes it into target.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800684
Alex Klein1699fab2022-09-08 08:46:06 -0600685 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600686 file_name: The file name in the remote directory self._image.
Alex Klein1699fab2022-09-08 08:46:06 -0600687 """
688 image_path = os.path.join(self._image, file_name)
689 with GsFileCopier(image_path) as generator:
690 try:
691 with open(generator.Target(), "rb") as fp:
692 # Always use GZIP as remote quick provision images are gzip
693 # compressed only.
694 self._WriteToTarget(
695 fp,
Mike Frysinger66306012022-04-22 15:23:13 -0400696 self._device.GetDecompressor(
697 cros_build_lib.CompressionType.GZIP
698 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600699 )
700 finally:
701 generator.CloseTarget()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800702
Amin Hassanid684e982021-02-26 11:10:58 -0800703
704class KernelUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600705 """A class to update the kernel partition on a Chromium OS device."""
Amin Hassanid684e982021-02-26 11:10:58 -0800706
Alex Klein1699fab2022-09-08 08:46:06 -0600707 def _GetPartitionName(self):
708 """See RawPartitionUpdater._GetPartitionName()."""
709 return constants.PART_KERN_B
Amin Hassanid684e982021-02-26 11:10:58 -0800710
Alex Klein1699fab2022-09-08 08:46:06 -0600711 def _GetRemotePartitionName(self):
712 """See RawPartitionUpdater._GetRemotePartitionName()."""
713 return constants.QUICK_PROVISION_PAYLOAD_KERNEL
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800714
Alex Klein1699fab2022-09-08 08:46:06 -0600715 def Revert(self):
716 """Reverts the kernel partition update."""
717 # There is nothing to do for reverting kernel partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800718
719
720class RootfsUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600721 """A class to update the root partition on a Chromium OS device."""
Amin Hassani75c5f942021-02-20 23:56:53 -0800722
Alex Klein1699fab2022-09-08 08:46:06 -0600723 def __init__(self, current_root: str, *args):
724 """Initializes the class.
Amin Hassani75c5f942021-02-20 23:56:53 -0800725
Alex Klein1699fab2022-09-08 08:46:06 -0600726 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600727 current_root: The current root device path.
728 *args: See PartitionUpdaterBase
Alex Klein1699fab2022-09-08 08:46:06 -0600729 """
730 super().__init__(*args)
Amin Hassani75c5f942021-02-20 23:56:53 -0800731
Alex Klein1699fab2022-09-08 08:46:06 -0600732 self._current_root = current_root
733 self._ran_postinst = False
Amin Hassani75c5f942021-02-20 23:56:53 -0800734
Alex Klein1699fab2022-09-08 08:46:06 -0600735 def _GetPartitionName(self):
736 """See RawPartitionUpdater._GetPartitionName()."""
737 return constants.PART_ROOT_A
Amin Hassani75c5f942021-02-20 23:56:53 -0800738
Alex Klein1699fab2022-09-08 08:46:06 -0600739 def _GetRemotePartitionName(self):
740 """See RawPartitionUpdater._GetRemotePartitionName()."""
741 return constants.QUICK_PROVISION_PAYLOAD_ROOTFS
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800742
Alex Klein1699fab2022-09-08 08:46:06 -0600743 def _Run(self):
744 """The function that does the job of rootfs partition update."""
745 with ProgressWatcher(self._device, self._target):
746 super()._Run()
Amin Hassani75c5f942021-02-20 23:56:53 -0800747
Alex Klein1699fab2022-09-08 08:46:06 -0600748 self._RunPostInst()
Amin Hassani75c5f942021-02-20 23:56:53 -0800749
Alex Klein1699fab2022-09-08 08:46:06 -0600750 def _OptimizePartLocation(self, offset: int, length: int):
751 """Optimizes the size of the root partition of the image.
Amin Hassani75c5f942021-02-20 23:56:53 -0800752
Alex Klein1699fab2022-09-08 08:46:06 -0600753 Normally the file system does not occupy the entire partition. Furthermore
754 we don't need the verity hash tree at the end of the root file system
755 because postinst will recreate it. This function reads the (approximate)
756 superblock of the ext4 partition and extracts the actual file system size in
757 the root partition.
758 """
759 superblock_size = 4096 * 2
760 with open(self._image, "rb") as r:
761 r.seek(offset)
762 with tempfile.NamedTemporaryFile(delete=False) as fp:
763 fp.write(r.read(superblock_size))
764 fp.close()
765 return offset, partition_lib.Ext2FileSystemSize(fp.name)
Amin Hassani75c5f942021-02-20 23:56:53 -0800766
Alex Klein1699fab2022-09-08 08:46:06 -0600767 def _RunPostInst(self, on_target: bool = True):
768 """Runs the postinst process in the root partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800769
Alex Klein1699fab2022-09-08 08:46:06 -0600770 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600771 on_target: If true the postinst is run on the target (inactive)
772 partition. This is used when doing normal updates. If false, the
773 postinst is run on the current (active) partition. This is used
774 when reverting an update.
Alex Klein1699fab2022-09-08 08:46:06 -0600775 """
776 try:
777 postinst_dir = "/"
778 partition = self._current_root
779 if on_target:
780 postinst_dir = self._device.run(
781 ["mktemp", "-d", "-p", self._device.work_dir],
782 capture_output=True,
783 ).stdout.strip()
784 self._device.run(
785 ["mount", "-o", "ro", self._target, postinst_dir]
786 )
787 partition = self._target
Amin Hassani75c5f942021-02-20 23:56:53 -0800788
Alex Klein1699fab2022-09-08 08:46:06 -0600789 self._ran_postinst = True
790 postinst = os.path.join(postinst_dir, "postinst")
791 result = self._device.run(
792 [postinst, partition], capture_output=True
793 )
Amin Hassani75c5f942021-02-20 23:56:53 -0800794
Alex Klein1699fab2022-09-08 08:46:06 -0600795 logging.debug(
796 "Postinst result on %s: \n%s", postinst, result.stdout
797 )
798 # DeviceImagerOperation will look for this log.
799 logging.info("Postinstall completed.")
800 finally:
801 if on_target:
802 self._device.run(["umount", postinst_dir])
Amin Hassani75c5f942021-02-20 23:56:53 -0800803
Alex Klein1699fab2022-09-08 08:46:06 -0600804 def Revert(self):
805 """Reverts the root update install."""
806 logging.info("Reverting the rootfs partition update.")
807 if self._ran_postinst:
808 # We don't have to do anything for revert if we haven't changed the kernel
809 # priorities yet.
810 self._RunPostInst(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800811
812
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000813class MiniOSUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600814 """A class to update the miniOS partition on a Chromium OS device."""
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000815
Alex Klein1699fab2022-09-08 08:46:06 -0600816 def __init__(self, *args):
817 """Initializes the class.
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000818
Alex Klein1699fab2022-09-08 08:46:06 -0600819 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600820 *args: See PartitionUpdaterBase
Alex Klein1699fab2022-09-08 08:46:06 -0600821 """
822 super().__init__(*args)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000823
Alex Klein1699fab2022-09-08 08:46:06 -0600824 self._ran_postinst = False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000825
Alex Klein1699fab2022-09-08 08:46:06 -0600826 def _GetPartitionName(self):
827 """See RawPartitionUpdater._GetPartitionName()."""
828 return constants.PART_MINIOS_A
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000829
Alex Klein1699fab2022-09-08 08:46:06 -0600830 def _GetRemotePartitionName(self):
831 """See RawPartitionUpdater._GetRemotePartitionName()."""
832 return constants.QUICK_PROVISION_PAYLOAD_MINIOS
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000833
Alex Klein1699fab2022-09-08 08:46:06 -0600834 def _Run(self):
835 """The function that does the job of rootfs partition update."""
836 if self._image_type == ImageType.FULL:
837 if self._MiniOSPartitionsExistInImage():
838 logging.info("Updating miniOS partition from local.")
839 super()._Run()
840 else:
841 logging.warning(
842 "Not updating miniOS partition as it does not exist."
843 )
844 return
845 elif self._image_type == ImageType.REMOTE_DIRECTORY:
846 if not gs.GSContext().Exists(
847 os.path.join(
848 self._image, constants.QUICK_PROVISION_PAYLOAD_MINIOS
849 )
850 ):
851 logging.warning("Not updating miniOS, missing remote files.")
852 return
853 elif not self._MiniOSPartitionsExist():
854 logging.warning("Not updating miniOS, missing partitions.")
855 return
856 else:
857 logging.info("Updating miniOS partition from remote.")
858 super()._Run()
859 else:
860 # Let super() handle this error.
861 super()._Run()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000862
Alex Klein1699fab2022-09-08 08:46:06 -0600863 self._RunPostInstall()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000864
Alex Klein1699fab2022-09-08 08:46:06 -0600865 def _RunPostInstall(self):
866 """The function will change the priority of the miniOS partitions."""
867 self._FlipMiniOSPriority()
868 self._ran_postinst = True
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000869
Alex Klein1699fab2022-09-08 08:46:06 -0600870 def Revert(self):
871 """Reverts the miniOS partition update."""
872 if self._ran_postinst:
873 self._FlipMiniOSPriority()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000874
Alex Klein1699fab2022-09-08 08:46:06 -0600875 def _GetMiniOSPriority(self):
876 return self._device.run(
877 ["crossystem", constants.MINIOS_PRIORITY]
878 ).stdout
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000879
Alex Klein1699fab2022-09-08 08:46:06 -0600880 def _SetMiniOSPriority(self, priority: str):
881 self._device.run(
882 ["crossystem", f"{constants.MINIOS_PRIORITY}={priority}"]
883 )
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000884
Alex Klein1699fab2022-09-08 08:46:06 -0600885 def _FlipMiniOSPriority(self):
886 inactive_minios_priority = (
887 "B" if self._GetMiniOSPriority() == "A" else "A"
888 )
889 logging.info("Setting miniOS priority to %s", inactive_minios_priority)
890 self._SetMiniOSPriority(inactive_minios_priority)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000891
Alex Klein1699fab2022-09-08 08:46:06 -0600892 def _MiniOSPartitionsExistInImage(self):
893 """Checks if miniOS partition exists in the image."""
894 d = cgpt.Disk.FromImage(self._image)
895 try:
896 d.GetPartitionByTypeGuid(cgpt.MINIOS_TYPE_GUID)
897 return True
898 except KeyError:
899 return False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000900
Alex Klein1699fab2022-09-08 08:46:06 -0600901 def _MiniOSPartitionsExist(self):
902 """Checks if the device has miniOS partitions."""
903 run = lambda x: self._device.run(x).stdout.strip()
904 device_drive = run(["rootdev", "-s", "-d"])
905 cmd = ["cgpt", "show", "-t", device_drive, "-i"]
906 return all(
907 (run(cmd + [p]) == cgpt.MINIOS_TYPE_GUID) for p in ("9", "10")
908 )
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700909
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000910
Amin Hassani74403082021-02-22 11:40:09 -0800911class StatefulPayloadGenerator(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600912 """A class for generating a stateful update payload in a separate thread."""
Amin Hassani74403082021-02-22 11:40:09 -0800913
Alex Klein1699fab2022-09-08 08:46:06 -0600914 def __init__(self, image: str):
915 """Initializes that class.
Amin Hassani74403082021-02-22 11:40:09 -0800916
Alex Klein1699fab2022-09-08 08:46:06 -0600917 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600918 image: The path to a local Chromium OS image.
Alex Klein1699fab2022-09-08 08:46:06 -0600919 """
920 super().__init__()
921 self._image = image
922
923 def run(self):
924 """Generates the stateful update and writes it into the output pipe."""
925 try:
926 paygen_stateful_payload_lib.GenerateStatefulPayload(
927 self._image, self._Source()
928 )
929 finally:
930 self._CloseSource()
Amin Hassani74403082021-02-22 11:40:09 -0800931
932
933class StatefulUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600934 """A class to update the stateful partition on a device."""
Amin Hassani74403082021-02-22 11:40:09 -0800935
Alex Klein1699fab2022-09-08 08:46:06 -0600936 def __init__(self, clobber_stateful: bool, *args):
937 """Initializes the class
Amin Hassani74403082021-02-22 11:40:09 -0800938
Alex Klein1699fab2022-09-08 08:46:06 -0600939 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600940 clobber_stateful: Whether to clobber the stateful or not.
941 *args: Look at PartitionUpdaterBase.
Alex Klein1699fab2022-09-08 08:46:06 -0600942 """
943 super().__init__(*args)
944 self._clobber_stateful = clobber_stateful
Amin Hassani74403082021-02-22 11:40:09 -0800945
Alex Klein1699fab2022-09-08 08:46:06 -0600946 def _Run(self):
947 """Reads/Downloads the stateful updates and writes it into the device."""
948 if self._image_type == ImageType.FULL:
949 generator_cls = StatefulPayloadGenerator
950 elif self._image_type == ImageType.REMOTE_DIRECTORY:
951 generator_cls = GsFileCopier
952 self._image = os.path.join(
953 self._image, paygen_stateful_payload_lib.STATEFUL_FILE
954 )
955 else:
956 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassani74403082021-02-22 11:40:09 -0800957
Alex Klein1699fab2022-09-08 08:46:06 -0600958 with generator_cls(self._image) as generator:
959 try:
960 updater = stateful_updater.StatefulUpdater(self._device)
961 updater.Update(
962 generator.Target(),
963 is_payload_on_device=False,
964 update_type=(
965 stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER
966 if self._clobber_stateful
967 else None
968 ),
969 )
970 finally:
971 generator.CloseTarget()
972
973 def Revert(self):
974 """Reverts the stateful partition update."""
975 logging.info("Reverting the stateful update.")
976 stateful_updater.StatefulUpdater(self._device).Reset()
Amin Hassani55970562021-02-22 20:49:13 -0800977
978
979class ProgressWatcher(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600980 """A class used for watching the progress of rootfs update."""
Amin Hassani55970562021-02-22 20:49:13 -0800981
Alex Klein1699fab2022-09-08 08:46:06 -0600982 def __init__(self, device, target_root: str):
983 """Initializes the class.
Amin Hassani55970562021-02-22 20:49:13 -0800984
Alex Klein1699fab2022-09-08 08:46:06 -0600985 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600986 device: The ChromiumOSDevice to be updated.
987 target_root: The target root partition to monitor the progress of.
Alex Klein1699fab2022-09-08 08:46:06 -0600988 """
989 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -0800990
Alex Klein1699fab2022-09-08 08:46:06 -0600991 self._device = device
992 self._target_root = target_root
993 self._exit = False
Amin Hassani55970562021-02-22 20:49:13 -0800994
Alex Klein1699fab2022-09-08 08:46:06 -0600995 def __enter__(self):
996 """Starts the thread."""
997 self.start()
998 return self
Amin Hassani55970562021-02-22 20:49:13 -0800999
Alex Klein1699fab2022-09-08 08:46:06 -06001000 def __exit__(self, *args, **kwargs):
1001 """Exists the thread."""
1002 self._exit = True
1003 self.join()
Amin Hassani55970562021-02-22 20:49:13 -08001004
Alex Klein1699fab2022-09-08 08:46:06 -06001005 def _ShouldExit(self):
1006 return self._exit
Amin Hassani55970562021-02-22 20:49:13 -08001007
Alex Klein1699fab2022-09-08 08:46:06 -06001008 def run(self):
1009 """Monitors the progress of the target root partitions' update.
Amin Hassani55970562021-02-22 20:49:13 -08001010
Alex Klein1699fab2022-09-08 08:46:06 -06001011 This is done by periodically, reading the fd position of the process that is
1012 writing into the target partition and reporting it back. Then the position
1013 is divided by the size of the block device to report an approximate
1014 progress.
1015 """
1016 cmd = ["blockdev", "--getsize64", self._target_root]
Amin Hassani55970562021-02-22 20:49:13 -08001017 output = self._device.run(cmd, capture_output=True).stdout.strip()
Alex Klein1699fab2022-09-08 08:46:06 -06001018 if output is None:
1019 raise Error(f"Cannot get the block device size from {output}.")
1020 dev_size = int(output)
1021
1022 # Using lsof to find out which process is writing to the target rootfs.
1023 cmd = ["lsof", "-t", self._target_root]
1024 while not self._ShouldExit():
1025 try:
1026 pid = self._device.run(cmd, capture_output=True).stdout.strip()
1027 if pid:
1028 break
1029 except cros_build_lib.RunCommandError:
1030 continue
1031 finally:
1032 time.sleep(1)
1033
1034 # Now that we know which process is writing to it, we can look the fdinfo of
1035 # stdout of that process to get its offset. We're assuming there will be no
1036 # seek, which is correct.
1037 cmd = ["cat", f"/proc/{pid}/fdinfo/1"]
1038 while not self._ShouldExit():
1039 try:
1040 output = self._device.run(
1041 cmd, capture_output=True
1042 ).stdout.strip()
1043 m = re.search(r"^pos:\s*(\d+)$", output, flags=re.M)
1044 if m:
1045 offset = int(m.group(1))
1046 # DeviceImagerOperation will look for this log.
1047 logging.info("RootFS progress: %f", offset / dev_size)
1048 except cros_build_lib.RunCommandError:
1049 continue
1050 finally:
1051 time.sleep(1)
Amin Hassani55970562021-02-22 20:49:13 -08001052
1053
1054class DeviceImagerOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -06001055 """A class to provide a progress bar for DeviceImager operation."""
Amin Hassani55970562021-02-22 20:49:13 -08001056
Alex Klein1699fab2022-09-08 08:46:06 -06001057 def __init__(self):
1058 """Initializes the class."""
1059 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -08001060
Alex Klein1699fab2022-09-08 08:46:06 -06001061 self._progress = 0.0
Amin Hassani55970562021-02-22 20:49:13 -08001062
Alex Klein1699fab2022-09-08 08:46:06 -06001063 def ParseOutput(self, output=None):
1064 """Override function to parse the output and provide progress.
Amin Hassani55970562021-02-22 20:49:13 -08001065
Alex Klein1699fab2022-09-08 08:46:06 -06001066 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001067 output: The stderr or stdout.
Alex Klein1699fab2022-09-08 08:46:06 -06001068 """
1069 output = self._stdout.read()
1070 match = re.findall(r"RootFS progress: (\d+(?:\.\d+)?)", output)
1071 if match:
1072 progress = float(match[0])
1073 self._progress = max(self._progress, progress)
Amin Hassani55970562021-02-22 20:49:13 -08001074
Alex Klein1699fab2022-09-08 08:46:06 -06001075 # If postinstall completes, move half of the remaining progress.
1076 if re.findall(r"Postinstall completed", output):
1077 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001078
Alex Klein1699fab2022-09-08 08:46:06 -06001079 # While waiting for reboot, each time, move half of the remaining progress.
1080 if re.findall(r"Unable to get new boot_id", output):
1081 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001082
Alex Klein1699fab2022-09-08 08:46:06 -06001083 if re.findall(r"DeviceImager completed.", output):
1084 self._progress = 1.0
Amin Hassani55970562021-02-22 20:49:13 -08001085
Alex Klein1699fab2022-09-08 08:46:06 -06001086 self.ProgressBar(self._progress)