blob: aaf311e9a24e654f03dd99c2e576f4d29fb918b3 [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
Jae Hoon Kim04a08062023-03-15 06:06:03 +00008import datetime
Amin Hassani92f6c4a2021-02-20 17:36:09 -08009import enum
Daichi Hironoc1a8fd32022-01-07 22:17:51 +090010from io import BytesIO
Chris McDonald14ac61d2021-07-21 11:49:56 -060011import logging
Amin Hassani92f6c4a2021-02-20 17:36:09 -080012import os
13import re
Mike Frysingerf1316bb2023-07-11 23:18:11 -040014import shutil
Amin Hassanid4b3ff82021-02-20 23:05:14 -080015import tempfile
16import threading
Amin Hassani55970562021-02-22 20:49:13 -080017import time
Daichi Hironoc1a8fd32022-01-07 22:17:51 +090018from typing import Dict, List, Tuple, Union
Amin Hassani92f6c4a2021-02-20 17:36:09 -080019
Amin Hassani55970562021-02-22 20:49:13 -080020from chromite.cli import command
Amin Hassanicf8f0042021-03-12 10:42:13 -080021from chromite.cli import flash
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000022from chromite.lib import cgpt
Amin Hassanid4b3ff82021-02-20 23:05:14 -080023from chromite.lib import constants
Amin Hassani92f6c4a2021-02-20 17:36:09 -080024from chromite.lib import cros_build_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080025from chromite.lib import gs
Amin Hassanid4b3ff82021-02-20 23:05:14 -080026from chromite.lib import image_lib
Amin Hassani55970562021-02-22 20:49:13 -080027from chromite.lib import operation
Amin Hassanid4b3ff82021-02-20 23:05:14 -080028from chromite.lib import osutils
Amin Hassani92f6c4a2021-02-20 17:36:09 -080029from chromite.lib import parallel
30from chromite.lib import remote_access
31from chromite.lib import retry_util
Amin Hassani74403082021-02-22 11:40:09 -080032from chromite.lib import stateful_updater
Amin Hassani75c5f942021-02-20 23:56:53 -080033from chromite.lib.paygen import partition_lib
Amin Hassani74403082021-02-22 11:40:09 -080034from chromite.lib.paygen import paygen_stateful_payload_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080035from chromite.lib.xbuddy import devserver_constants
36from chromite.lib.xbuddy import xbuddy
Alex Klein18ef1212021-10-14 12:49:02 -060037from chromite.utils import timer
Amin Hassani92f6c4a2021-02-20 17:36:09 -080038
39
Amin Hassani92f6c4a2021-02-20 17:36:09 -080040class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060041 """Thrown when there is a general Chromium OS-specific flash error."""
Amin Hassani92f6c4a2021-02-20 17:36:09 -080042
43
44class ImageType(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060045 """Type of the image that is used for flashing the device."""
Amin Hassani92f6c4a2021-02-20 17:36:09 -080046
Alex Klein1699fab2022-09-08 08:46:06 -060047 # The full image on disk (e.g. chromiumos_test_image.bin).
48 FULL = 0
49 # The remote directory path
50 # (e.g gs://chromeos-image-archive/eve-release/R90-x.x.x)
51 REMOTE_DIRECTORY = 1
Amin Hassani92f6c4a2021-02-20 17:36:09 -080052
53
54class Partition(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060055 """An enum for partition types like kernel and rootfs."""
56
57 KERNEL = 0
58 ROOTFS = 1
59 MINIOS = 2
Amin Hassani92f6c4a2021-02-20 17:36:09 -080060
61
Alex Klein074f94f2023-06-22 10:32:06 -060062class DeviceImager:
Alex Klein1699fab2022-09-08 08:46:06 -060063 """A class to flash a Chromium OS device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080064
Alex Klein975e86c2023-01-23 16:49:10 -070065 This utility uses parallelism as much as possible to achieve its goal as
66 fast as possible. For example, it uses parallel compressors, parallel
67 transfers, and simultaneous pipes.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080068 """
69
Alex Klein1699fab2022-09-08 08:46:06 -060070 # The parameters of the kernel and rootfs's two main partitions.
71 A = {Partition.KERNEL: 2, Partition.ROOTFS: 3}
72 B = {Partition.KERNEL: 4, Partition.ROOTFS: 5}
Amin Hassani92f6c4a2021-02-20 17:36:09 -080073
Alex Klein1699fab2022-09-08 08:46:06 -060074 MINIOS_A = {Partition.MINIOS: 9}
75 MINIOS_B = {Partition.MINIOS: 10}
Amin Hassani92f6c4a2021-02-20 17:36:09 -080076
Alex Klein1699fab2022-09-08 08:46:06 -060077 def __init__(
78 self,
79 device,
80 image: str,
81 board: str = None,
82 version: str = None,
83 no_rootfs_update: bool = False,
84 no_stateful_update: bool = False,
85 no_minios_update: bool = False,
86 no_reboot: bool = False,
87 disable_verification: bool = False,
88 clobber_stateful: bool = False,
89 clear_tpm_owner: bool = False,
90 delta: bool = False,
Jae Hoon Kim04a08062023-03-15 06:06:03 +000091 reboot_timeout: datetime.timedelta = None,
Alex Klein1699fab2022-09-08 08:46:06 -060092 ):
93 """Initialize DeviceImager for flashing a Chromium OS device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080094
Alex Klein1699fab2022-09-08 08:46:06 -060095 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -060096 device: The ChromiumOSDevice to be updated.
97 image: The target image path (can be xBuddy path).
98 board: Board to use.
99 version: Image version to use.
100 no_rootfs_update: Whether to do rootfs partition update.
101 no_stateful_update: Whether to do stateful partition update.
102 no_minios_update: Whether to do minios partition update.
103 no_reboot: Whether to reboot device after update, default True.
104 disable_verification: Whether to disable rootfs verification on the
105 device.
106 clobber_stateful: Whether to do a clean stateful partition.
107 clear_tpm_owner: If true, it will clear the TPM owner on reboot.
108 delta: Whether to use delta compression when transferring image
109 bytes.
Jae Hoon Kim04a08062023-03-15 06:06:03 +0000110 reboot_timeout: The timeout for reboot.
Alex Klein1699fab2022-09-08 08:46:06 -0600111 """
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800112
Alex Klein1699fab2022-09-08 08:46:06 -0600113 self._device = device
114 self._image = image
115 self._board = board
116 self._version = version
117 self._no_rootfs_update = no_rootfs_update
118 self._no_stateful_update = no_stateful_update
119 self._no_minios_update = no_minios_update
120 self._no_reboot = no_reboot
121 self._disable_verification = disable_verification
122 self._clobber_stateful = clobber_stateful
123 self._clear_tpm_owner = clear_tpm_owner
Jae Hoon Kim04a08062023-03-15 06:06:03 +0000124 self._reboot_timeout = reboot_timeout
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800125
Alex Klein1699fab2022-09-08 08:46:06 -0600126 self._image_type = None
127 self._inactive_state = None
128 self._delta = delta
Daichi Hirono28831b3b2022-04-07 12:41:11 +0900129
Alex Klein1699fab2022-09-08 08:46:06 -0600130 def Run(self):
131 """Update the device with image of specific version."""
132 self._LocateImage()
133 logging.notice(
134 "Preparing to update the remote device %s with image %s",
135 self._device.hostname,
136 self._image,
137 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800138
Alex Klein1699fab2022-09-08 08:46:06 -0600139 try:
140 if command.UseProgressBar():
141 op = DeviceImagerOperation()
142 op.Run(self._Run)
143 else:
144 self._Run()
145 except Exception as e:
146 raise Error(f"DeviceImager Failed with error: {e}")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800147
Alex Klein1699fab2022-09-08 08:46:06 -0600148 # DeviceImagerOperation will look for this log.
149 logging.info("DeviceImager completed.")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800150
Alex Klein1699fab2022-09-08 08:46:06 -0600151 def _Run(self):
152 """Runs the various operations to install the image on device."""
153 # TODO(b/228389041): Switch to delta compression if self._delta is True
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800154
Alex Klein1699fab2022-09-08 08:46:06 -0600155 self._InstallPartitions()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800156
Alex Klein1699fab2022-09-08 08:46:06 -0600157 if self._clear_tpm_owner:
158 self._device.ClearTpmOwner()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800159
Daichi Hirono1d45ed52023-01-20 17:30:26 +0900160 if self._disable_verification:
161 # DisableRootfsVerification internally invokes Reboot().
Jae Hoon Kim04a08062023-03-15 06:06:03 +0000162 self._device.DisableRootfsVerification(
163 timeout_sec=self._reboot_timeout.total_seconds()
164 )
Daichi Hirono1d45ed52023-01-20 17:30:26 +0900165 self._VerifyBootExpectations()
166 elif not self._no_reboot:
Alex Klein1699fab2022-09-08 08:46:06 -0600167 self._Reboot()
168 self._VerifyBootExpectations()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800169
Alex Klein1699fab2022-09-08 08:46:06 -0600170 def _LocateImage(self):
171 """Locates the path to the final image(s) that need to be installed.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800172
Alex Klein1699fab2022-09-08 08:46:06 -0600173 If the paths is local, the image should be the Chromium OS GPT image
Alex Klein975e86c2023-01-23 16:49:10 -0700174 (e.g. chromiumos_test_image.bin). If the path is remote, it should be
175 the remote directory where we can find the quick-provision and stateful
176 update files (e.g. gs://chromeos-image-archive/eve-release/R90-x.x.x).
Amin Hassanicf8f0042021-03-12 10:42:13 -0800177
Alex Klein975e86c2023-01-23 16:49:10 -0700178 NOTE: At this point there is no caching involved. Hence we always
179 download the partition payloads or extract them from the Chromium OS
180 image.
Alex Klein1699fab2022-09-08 08:46:06 -0600181 """
182 if os.path.isfile(self._image):
183 self._image_type = ImageType.FULL
184 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800185
Alex Klein1699fab2022-09-08 08:46:06 -0600186 # TODO(b/172212406): We could potentially also allow this by searching
Alex Klein975e86c2023-01-23 16:49:10 -0700187 # through the directory to see whether we have quick-provision and
188 # stateful payloads. This only makes sense when a user has their
189 # workstation at home and doesn't want to incur the bandwidth cost of
190 # downloading the same image multiple times. For that, they can simply
191 # download the GPT image image first and flash that instead.
Alex Klein1699fab2022-09-08 08:46:06 -0600192 if os.path.isdir(self._image):
193 raise ValueError(
194 f"{self._image}: input must be a disk image, not a directory."
195 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800196
Alex Klein1699fab2022-09-08 08:46:06 -0600197 if gs.PathIsGs(self._image):
198 # TODO(b/172212406): Check whether it is a directory. If it wasn't a
Alex Klein975e86c2023-01-23 16:49:10 -0700199 # directory download the image into some temp location and use it
200 # instead.
Alex Klein1699fab2022-09-08 08:46:06 -0600201 self._image_type = ImageType.REMOTE_DIRECTORY
202 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800203
Alex Klein1699fab2022-09-08 08:46:06 -0600204 # Assuming it is an xBuddy path.
205 board = cros_build_lib.GetBoard(
206 device_board=self._device.board or flash.GetDefaultBoard(),
207 override_board=self._board,
208 force=True,
209 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800210
Alex Klein1699fab2022-09-08 08:46:06 -0600211 xb = xbuddy.XBuddy(board=board, version=self._version)
212 build_id, local_file = xb.Translate([self._image])
213 if build_id is None:
214 raise Error(f"{self._image}: unable to find matching xBuddy path.")
215 logging.info("XBuddy path translated to build ID %s", build_id)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800216
Alex Klein1699fab2022-09-08 08:46:06 -0600217 if local_file:
218 self._image = local_file
219 self._image_type = ImageType.FULL
220 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800221
Alex Klein1699fab2022-09-08 08:46:06 -0600222 self._image = f"{devserver_constants.GS_IMAGE_DIR}/{build_id}"
223 self._image_type = ImageType.REMOTE_DIRECTORY
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800224
Alex Klein1699fab2022-09-08 08:46:06 -0600225 def _SplitDevPath(self, path: str) -> Tuple[str, int]:
226 """Splits the given /dev/x path into prefix and the dev number.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800227
Alex Klein1699fab2022-09-08 08:46:06 -0600228 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600229 path: The path to a block dev device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800230
Alex Klein1699fab2022-09-08 08:46:06 -0600231 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600232 A tuple of representing the prefix and the index of the dev path.
233 e.g.: '/dev/mmcblk0p1' -> ['/dev/mmcblk0p', 1]
Alex Klein1699fab2022-09-08 08:46:06 -0600234 """
235 match = re.search(r"(.*)([0-9]+)$", path)
236 if match is None:
237 raise Error(f"{path}: Could not parse root dev path.")
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000238
Alex Klein1699fab2022-09-08 08:46:06 -0600239 return match.group(1), int(match.group(2))
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000240
Alex Klein1699fab2022-09-08 08:46:06 -0600241 def _GetKernelState(self, root_num: int) -> Tuple[Dict, Dict]:
242 """Returns the kernel state.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800243
Alex Klein1699fab2022-09-08 08:46:06 -0600244 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600245 A tuple of two dictionaries: The current active kernel state and the
246 inactive kernel state. (Look at A and B constants in this class.)
Alex Klein1699fab2022-09-08 08:46:06 -0600247 """
248 if root_num == self.A[Partition.ROOTFS]:
249 return self.A, self.B
250 elif root_num == self.B[Partition.ROOTFS]:
251 return self.B, self.A
252 else:
253 raise Error(f"Invalid root partition number {root_num}")
Amin Hassanid684e982021-02-26 11:10:58 -0800254
Alex Klein1699fab2022-09-08 08:46:06 -0600255 def _GetMiniOSState(self, minios_num: int) -> Tuple[Dict, Dict]:
256 """Returns the miniOS state.
Amin Hassani75c5f942021-02-20 23:56:53 -0800257
Alex Klein1699fab2022-09-08 08:46:06 -0600258 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600259 A tuple of dictionaries: The current active miniOS state and the
260 inactive miniOS state.
Alex Klein1699fab2022-09-08 08:46:06 -0600261 """
262 if minios_num == self.MINIOS_A[Partition.MINIOS]:
263 return self.MINIOS_A, self.MINIOS_B
264 elif minios_num == self.MINIOS_B[Partition.MINIOS]:
265 return self.MINIOS_B, self.MINIOS_A
266 else:
267 raise Error(f"Invalid minios partition number {minios_num}")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800268
Alex Klein1699fab2022-09-08 08:46:06 -0600269 def _InstallPartitions(self):
270 """The main method that installs the partitions of a Chrome OS device.
Amin Hassani74403082021-02-22 11:40:09 -0800271
Alex Klein1699fab2022-09-08 08:46:06 -0600272 It uses parallelism to install the partitions as fast as possible.
273 """
274 prefix, root_num = self._SplitDevPath(self._device.root_dev)
275 active_state, self._inactive_state = self._GetKernelState(root_num)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000276
Alex Klein1699fab2022-09-08 08:46:06 -0600277 updaters = []
278 if not self._no_rootfs_update:
279 current_root = prefix + str(active_state[Partition.ROOTFS])
280 target_root = prefix + str(self._inactive_state[Partition.ROOTFS])
281 updaters.append(
282 RootfsUpdater(
283 current_root,
284 self._device,
285 self._image,
286 self._image_type,
287 target_root,
288 )
289 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800290
Alex Klein1699fab2022-09-08 08:46:06 -0600291 target_kernel = prefix + str(self._inactive_state[Partition.KERNEL])
292 updaters.append(
293 KernelUpdater(
294 self._device, self._image, self._image_type, target_kernel
295 )
296 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800297
Alex Klein1699fab2022-09-08 08:46:06 -0600298 if not self._no_stateful_update:
299 updaters.append(
300 StatefulUpdater(
301 self._clobber_stateful,
302 self._device,
303 self._image,
304 self._image_type,
305 None,
306 )
307 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800308
Alex Klein1699fab2022-09-08 08:46:06 -0600309 if not self._no_minios_update:
310 minios_priority = self._device.run(
311 ["crossystem", constants.MINIOS_PRIORITY]
312 ).stdout
313 if minios_priority not in ["A", "B"]:
314 logging.warning(
315 "Skipping miniOS flash due to missing priority."
316 )
317 else:
318 # Reference disk_layout_v3 for partition numbering.
319 _, inactive_minios_state = self._GetMiniOSState(
320 9 if minios_priority == "A" else 10
321 )
322 target_minios = prefix + str(
323 inactive_minios_state[Partition.MINIOS]
324 )
325 minios_updater = MiniOSUpdater(
326 self._device, self._image, self._image_type, target_minios
327 )
328 updaters.append(minios_updater)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800329
Alex Klein975e86c2023-01-23 16:49:10 -0700330 # Retry the partitions updates that failed, in case a transient error
331 # (like SSH drop, etc) caused the error.
Alex Klein1699fab2022-09-08 08:46:06 -0600332 num_retries = 1
333 try:
334 retry_util.RetryException(
335 Error,
336 num_retries,
337 parallel.RunParallelSteps,
338 (x.Run for x in updaters if not x.IsFinished()),
339 halt_on_error=True,
340 )
341 except Exception:
Alex Klein975e86c2023-01-23 16:49:10 -0700342 # If one of the partitions failed to be installed, revert all
343 # partitions.
Alex Klein1699fab2022-09-08 08:46:06 -0600344 parallel.RunParallelSteps(x.Revert for x in updaters)
345 raise
346
347 def _Reboot(self):
348 """Reboots the device."""
349 try:
Jae Hoon Kim04a08062023-03-15 06:06:03 +0000350 self._device.Reboot(
351 timeout_sec=self._reboot_timeout.total_seconds()
352 )
Alex Klein1699fab2022-09-08 08:46:06 -0600353 except remote_access.RebootError:
354 raise Error(
355 "Could not recover from reboot. Once example reason"
356 " could be the image provided was a non-test image"
357 " or the system failed to boot after the update."
358 )
359 except Exception as e:
360 raise Error(f"Failed to reboot to the device with error: {e}")
361
362 def _VerifyBootExpectations(self):
363 """Verify that we fully booted into the expected kernel state."""
364 # Discover the newly active kernel.
365 _, root_num = self._SplitDevPath(self._device.root_dev)
366 active_state, _ = self._GetKernelState(root_num)
367
368 # If this happens, we should rollback.
369 if active_state != self._inactive_state:
370 raise Error("The expected kernel state after update is invalid.")
371
372 logging.info("Verified boot expectations.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800373
374
375class ReaderBase(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600376 """The base class for reading different inputs and writing into output.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800377
Alex Klein975e86c2023-01-23 16:49:10 -0700378 This class extends threading.Thread, so it will be run on its own thread.
379 Also it can be used as a context manager. Internally, it opens necessary
380 files for writing to and reading from. This class cannot be instantiated, it
381 needs to be sub-classed first to provide necessary function implementations.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800382 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800383
Alex Klein1699fab2022-09-08 08:46:06 -0600384 def __init__(self, use_named_pipes: bool = False):
385 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800386
Alex Klein1699fab2022-09-08 08:46:06 -0600387 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600388 use_named_pipes: Whether to use a named pipe or anonymous file
Alex Klein1699fab2022-09-08 08:46:06 -0600389 descriptors.
390 """
391 super().__init__()
392 self._use_named_pipes = use_named_pipes
393 self._pipe_target = None
394 self._pipe_source = None
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800395
Alex Klein1699fab2022-09-08 08:46:06 -0600396 def __del__(self):
397 """Destructor.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800398
Alex Klein1699fab2022-09-08 08:46:06 -0600399 Make sure to clean up any named pipes we might have created.
400 """
401 if self._use_named_pipes:
402 osutils.SafeUnlink(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800403
Alex Klein1699fab2022-09-08 08:46:06 -0600404 def __enter__(self):
405 """Enters the context manager"""
406 if self._use_named_pipes:
Alex Klein975e86c2023-01-23 16:49:10 -0700407 # There is no need for the temp file, we only need its path. So the
408 # named pipe is created after this temp file is deleted.
Alex Klein1699fab2022-09-08 08:46:06 -0600409 with tempfile.NamedTemporaryFile(
410 prefix="chromite-device-imager"
411 ) as fp:
412 self._pipe_target = self._pipe_source = fp.name
413 os.mkfifo(self._pipe_target)
414 else:
415 self._pipe_target, self._pipe_source = os.pipe()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800416
Alex Klein1699fab2022-09-08 08:46:06 -0600417 self.start()
418 return self
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800419
Alex Klein1699fab2022-09-08 08:46:06 -0600420 def __exit__(self, *args, **kwargs):
421 """Exits the context manager."""
422 self.join()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800423
Alex Klein1699fab2022-09-08 08:46:06 -0600424 def _Source(self):
425 """Returns the source pipe to write data into.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800426
Alex Klein1699fab2022-09-08 08:46:06 -0600427 Sub-classes can use this function to determine where to write their data
428 into.
429 """
430 return self._pipe_source
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800431
Alex Klein1699fab2022-09-08 08:46:06 -0600432 def _CloseSource(self):
433 """Closes the source pipe.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800434
Alex Klein975e86c2023-01-23 16:49:10 -0700435 Sub-classes should use this function to close the pipe after they are
436 done writing into it. Failure to do so may result reader of the data to
437 hang indefinitely.
Alex Klein1699fab2022-09-08 08:46:06 -0600438 """
439 if not self._use_named_pipes:
440 os.close(self._pipe_source)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800441
Alex Klein1699fab2022-09-08 08:46:06 -0600442 def Target(self):
443 """Returns the target pipe to read data from.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800444
Alex Klein1699fab2022-09-08 08:46:06 -0600445 Users of this class can use this path to read data from.
446 """
447 return self._pipe_target
448
449 def CloseTarget(self):
450 """Closes the target pipe.
451
Alex Klein975e86c2023-01-23 16:49:10 -0700452 Users of this class should use this function to close the pipe after
453 they are done reading from it.
Alex Klein1699fab2022-09-08 08:46:06 -0600454 """
455 if self._use_named_pipes:
456 os.remove(self._pipe_target)
457 else:
458 os.close(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800459
460
461class PartialFileReader(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600462 """A class to read specific offset and length from a file and compress it.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800463
Alex Klein1699fab2022-09-08 08:46:06 -0600464 This class can be used to read from specific location and length in a file
Alex Klein975e86c2023-01-23 16:49:10 -0700465 (e.g. A partition in a GPT image). Then it compresses the input and writes
466 it out (to a pipe). Look at the base class for more information.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800467 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800468
Alex Klein1699fab2022-09-08 08:46:06 -0600469 # The offset of different partitions in a Chromium OS image does not always
Alex Klein975e86c2023-01-23 16:49:10 -0700470 # align to larger values like 4096. It seems that 512 is the maximum value
471 # to be divisible by partition offsets. This size should not be increased
472 # just for 'performance reasons'. Since we are doing everything in parallel,
473 # in practice there is not much difference between this and larger block
474 # sizes as parallelism hides the possible extra latency provided by smaller
475 # block sizes.
Alex Klein1699fab2022-09-08 08:46:06 -0600476 _BLOCK_SIZE = 512
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800477
Alex Klein1699fab2022-09-08 08:46:06 -0600478 def __init__(
479 self,
480 image: str,
481 offset: int,
482 length: int,
483 compression_command: List[str],
484 ):
485 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800486
Alex Klein1699fab2022-09-08 08:46:06 -0600487 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600488 image: The path to an image (local or remote directory).
489 offset: The offset (in bytes) to read from the image.
490 length: The length (in bytes) to read from the image.
491 compression_command: The command to compress transferred bytes.
Alex Klein1699fab2022-09-08 08:46:06 -0600492 """
493 super().__init__()
494
495 self._image = image
496 self._offset = offset
497 self._length = length
498 self._compression_command = compression_command
499
500 def run(self):
501 """Runs the reading and compression."""
Mike Frysinger906119e2022-12-27 18:10:23 -0500502 data = osutils.ReadFile(
503 self._image, mode="rb", size=self._length, seek=self._offset
504 )
Alex Klein1699fab2022-09-08 08:46:06 -0600505 try:
Mike Frysinger906119e2022-12-27 18:10:23 -0500506 cros_build_lib.run(
507 self._compression_command, input=data, stdout=self._Source()
508 )
Alex Klein1699fab2022-09-08 08:46:06 -0600509 finally:
510 self._CloseSource()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800511
512
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800513class GsFileCopier(ReaderBase):
Alex Klein975e86c2023-01-23 16:49:10 -0700514 """A class to download gzip compressed file from GS bucket into a pipe."""
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800515
Alex Klein1699fab2022-09-08 08:46:06 -0600516 def __init__(self, image: str):
517 """Initializes the class.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800518
Alex Klein1699fab2022-09-08 08:46:06 -0600519 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600520 image: The path to an image (local or remote directory).
Alex Klein1699fab2022-09-08 08:46:06 -0600521 """
522 super().__init__(use_named_pipes=True)
523 self._image = image
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800524
Alex Klein1699fab2022-09-08 08:46:06 -0600525 def run(self):
526 """Runs the download and write into the output pipe."""
527 try:
Mike Frysingerf1316bb2023-07-11 23:18:11 -0400528 if gs.PathIsGs(self._image):
529 gs.GSContext().Copy(self._image, self._Source())
530 else:
531 with open(self._image, "rb") as fsrc:
532 with open(self._Source(), "wb") as fdst:
533 shutil.copyfileobj(fsrc, fdst)
Alex Klein1699fab2022-09-08 08:46:06 -0600534 finally:
535 self._CloseSource()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800536
537
Alex Klein074f94f2023-06-22 10:32:06 -0600538class PartitionUpdaterBase:
Alex Klein1699fab2022-09-08 08:46:06 -0600539 """A base abstract class to use for installing an image into a partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800540
Alex Klein1699fab2022-09-08 08:46:06 -0600541 Sub-classes should implement the abstract methods to provide the core
542 functionality.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800543 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800544
Alex Klein1699fab2022-09-08 08:46:06 -0600545 def __init__(self, device, image: str, image_type, target: str):
Alex Klein975e86c2023-01-23 16:49:10 -0700546 """Initializes this base class with the most commonly needed values.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800547
Alex Klein1699fab2022-09-08 08:46:06 -0600548 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600549 device: The ChromiumOSDevice to be updated.
550 image: The target image path for the partition update.
551 image_type: The type of the image (ImageType).
552 target: The target path (e.g. block dev) to install the update.
Alex Klein1699fab2022-09-08 08:46:06 -0600553 """
554 self._device = device
555 self._image = image
556 self._image_type = image_type
557 self._target = target
558 self._finished = False
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800559
Alex Klein1699fab2022-09-08 08:46:06 -0600560 def Run(self):
561 """The main function that does the partition update job."""
562 with timer.Timer() as t:
563 try:
564 self._Run()
565 finally:
566 self._finished = True
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800567
Alex Klein1699fab2022-09-08 08:46:06 -0600568 logging.debug("Completed %s in %s", self.__class__.__name__, t)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800569
Alex Klein1699fab2022-09-08 08:46:06 -0600570 @abc.abstractmethod
571 def _Run(self):
572 """The method that need to be implemented by sub-classes."""
573 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800574
Alex Klein1699fab2022-09-08 08:46:06 -0600575 def IsFinished(self):
576 """Returns whether the partition update has been successful."""
577 return self._finished
578
579 @abc.abstractmethod
580 def Revert(self):
581 """Reverts the partition update.
582
Alex Klein975e86c2023-01-23 16:49:10 -0700583 Subclasses need to implement this function to provide revert capability.
Alex Klein1699fab2022-09-08 08:46:06 -0600584 """
585 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800586
587
588class RawPartitionUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600589 """A class to update a raw partition on a Chromium OS device."""
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800590
Alex Klein1699fab2022-09-08 08:46:06 -0600591 def _Run(self):
592 """The function that does the job of kernel partition update."""
593 if self._image_type == ImageType.FULL:
594 self._CopyPartitionFromImage(self._GetPartitionName())
595 elif self._image_type == ImageType.REMOTE_DIRECTORY:
596 self._RedirectPartition(self._GetRemotePartitionName())
597 else:
598 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800599
Alex Klein1699fab2022-09-08 08:46:06 -0600600 def _GetPartitionName(self):
601 """Returns the name of the partition in a Chromium OS GPT layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800602
Alex Klein1699fab2022-09-08 08:46:06 -0600603 Subclasses should override this function to return correct name.
604 """
605 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800606
Alex Klein1699fab2022-09-08 08:46:06 -0600607 def _CopyPartitionFromImage(self, part_name: str):
608 """Updates the device's partition from a local Chromium OS image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800609
Alex Klein1699fab2022-09-08 08:46:06 -0600610 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600611 part_name: The name of the partition in the source image that needs
612 to be extracted.
Alex Klein1699fab2022-09-08 08:46:06 -0600613 """
614 offset, length = self._GetPartLocation(part_name)
615 offset, length = self._OptimizePartLocation(offset, length)
616 compressor, decompressor = self._GetCompressionAndDecompression()
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900617
Alex Klein1699fab2022-09-08 08:46:06 -0600618 with PartialFileReader(
619 self._image, offset, length, compressor
620 ) as generator:
621 try:
622 self._WriteToTarget(generator.Target(), decompressor)
623 finally:
624 generator.CloseTarget()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800625
Alex Klein1699fab2022-09-08 08:46:06 -0600626 def _GetCompressionAndDecompression(self) -> Tuple[List[str], List[str]]:
627 """Returns compression / decompression commands."""
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900628
Alex Klein1699fab2022-09-08 08:46:06 -0600629 return (
Mike Frysinger66306012022-04-22 15:23:13 -0400630 [
631 cros_build_lib.FindCompressor(
632 cros_build_lib.CompressionType.GZIP
633 )
634 ],
635 self._device.GetDecompressor(cros_build_lib.CompressionType.GZIP),
Alex Klein1699fab2022-09-08 08:46:06 -0600636 )
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900637
Alex Klein1699fab2022-09-08 08:46:06 -0600638 def _WriteToTarget(
639 self, source: Union[int, BytesIO], decompress_command: List[str]
640 ) -> None:
641 """Writes bytes source to the target device on DUT.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800642
Alex Klein1699fab2022-09-08 08:46:06 -0600643 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600644 A string command to run on a device to read data from stdin,
645 uncompress it and write it to the target partition.
Alex Klein1699fab2022-09-08 08:46:06 -0600646 """
Alex Klein1699fab2022-09-08 08:46:06 -0600647 cmd = " ".join(
648 [
649 *decompress_command,
650 "|",
651 "dd",
652 "bs=1M",
Alex Klein1699fab2022-09-08 08:46:06 -0600653 f"of={self._target}",
654 ]
655 )
656 self._device.run(cmd, input=source, shell=True)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800657
Alex Klein1699fab2022-09-08 08:46:06 -0600658 def _GetPartLocation(self, part_name: str):
659 """Extracts the location and size of the raw partition from the image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800660
Alex Klein1699fab2022-09-08 08:46:06 -0600661 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600662 part_name: The name of the partition in the source image that needs
663 to be extracted.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800664
Alex Klein1699fab2022-09-08 08:46:06 -0600665 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600666 A tuple of offset and length (in bytes) from the image.
Alex Klein1699fab2022-09-08 08:46:06 -0600667 """
668 try:
669 parts = image_lib.GetImageDiskPartitionInfo(self._image)
670 part_info = [p for p in parts if p.name == part_name][0]
671 except IndexError:
672 raise Error(f"No partition named {part_name} found.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800673
Alex Klein1699fab2022-09-08 08:46:06 -0600674 return int(part_info.start), int(part_info.size)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800675
Alex Klein1699fab2022-09-08 08:46:06 -0600676 def _GetRemotePartitionName(self):
677 """Returns the name of the quick-provision partition file.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800678
Alex Klein1699fab2022-09-08 08:46:06 -0600679 Subclasses should override this function to return correct name.
680 """
681 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800682
Alex Klein1699fab2022-09-08 08:46:06 -0600683 def _OptimizePartLocation(self, offset: int, length: int):
684 """Optimizes the offset and length of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800685
Alex Klein975e86c2023-01-23 16:49:10 -0700686 Subclasses can override this to provide better offset/length than what
687 is defined in the PGT partition layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800688
Alex Klein1699fab2022-09-08 08:46:06 -0600689 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600690 offset: The offset (in bytes) of the partition in the image.
691 length: The length (in bytes) of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800692
Alex Klein1699fab2022-09-08 08:46:06 -0600693 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600694 A tuple of offset and length (in bytes) from the image.
Alex Klein1699fab2022-09-08 08:46:06 -0600695 """
696 return offset, length
Amin Hassanid684e982021-02-26 11:10:58 -0800697
Alex Klein1699fab2022-09-08 08:46:06 -0600698 def _RedirectPartition(self, file_name: str):
699 """Downloads the partition from a remote path and writes it into target.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800700
Alex Klein1699fab2022-09-08 08:46:06 -0600701 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600702 file_name: The file name in the remote directory self._image.
Alex Klein1699fab2022-09-08 08:46:06 -0600703 """
704 image_path = os.path.join(self._image, file_name)
705 with GsFileCopier(image_path) as generator:
706 try:
707 with open(generator.Target(), "rb") as fp:
708 # Always use GZIP as remote quick provision images are gzip
709 # compressed only.
710 self._WriteToTarget(
711 fp,
Mike Frysinger66306012022-04-22 15:23:13 -0400712 self._device.GetDecompressor(
713 cros_build_lib.CompressionType.GZIP
714 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600715 )
716 finally:
717 generator.CloseTarget()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800718
Amin Hassanid684e982021-02-26 11:10:58 -0800719
720class KernelUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600721 """A class to update the kernel partition on a Chromium OS device."""
Amin Hassanid684e982021-02-26 11:10:58 -0800722
Alex Klein1699fab2022-09-08 08:46:06 -0600723 def _GetPartitionName(self):
724 """See RawPartitionUpdater._GetPartitionName()."""
725 return constants.PART_KERN_B
Amin Hassanid684e982021-02-26 11:10:58 -0800726
Alex Klein1699fab2022-09-08 08:46:06 -0600727 def _GetRemotePartitionName(self):
728 """See RawPartitionUpdater._GetRemotePartitionName()."""
729 return constants.QUICK_PROVISION_PAYLOAD_KERNEL
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800730
Alex Klein1699fab2022-09-08 08:46:06 -0600731 def Revert(self):
732 """Reverts the kernel partition update."""
733 # There is nothing to do for reverting kernel partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800734
735
736class RootfsUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600737 """A class to update the root partition on a Chromium OS device."""
Amin Hassani75c5f942021-02-20 23:56:53 -0800738
Alex Klein1699fab2022-09-08 08:46:06 -0600739 def __init__(self, current_root: str, *args):
740 """Initializes the class.
Amin Hassani75c5f942021-02-20 23:56:53 -0800741
Alex Klein1699fab2022-09-08 08:46:06 -0600742 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600743 current_root: The current root device path.
744 *args: See PartitionUpdaterBase
Alex Klein1699fab2022-09-08 08:46:06 -0600745 """
746 super().__init__(*args)
Amin Hassani75c5f942021-02-20 23:56:53 -0800747
Alex Klein1699fab2022-09-08 08:46:06 -0600748 self._current_root = current_root
749 self._ran_postinst = False
Amin Hassani75c5f942021-02-20 23:56:53 -0800750
Alex Klein1699fab2022-09-08 08:46:06 -0600751 def _GetPartitionName(self):
752 """See RawPartitionUpdater._GetPartitionName()."""
753 return constants.PART_ROOT_A
Amin Hassani75c5f942021-02-20 23:56:53 -0800754
Alex Klein1699fab2022-09-08 08:46:06 -0600755 def _GetRemotePartitionName(self):
756 """See RawPartitionUpdater._GetRemotePartitionName()."""
757 return constants.QUICK_PROVISION_PAYLOAD_ROOTFS
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800758
Alex Klein1699fab2022-09-08 08:46:06 -0600759 def _Run(self):
760 """The function that does the job of rootfs partition update."""
761 with ProgressWatcher(self._device, self._target):
762 super()._Run()
Amin Hassani75c5f942021-02-20 23:56:53 -0800763
Alex Klein1699fab2022-09-08 08:46:06 -0600764 self._RunPostInst()
Amin Hassani75c5f942021-02-20 23:56:53 -0800765
Alex Klein1699fab2022-09-08 08:46:06 -0600766 def _OptimizePartLocation(self, offset: int, length: int):
767 """Optimizes the size of the root partition of the image.
Amin Hassani75c5f942021-02-20 23:56:53 -0800768
Alex Klein975e86c2023-01-23 16:49:10 -0700769 Normally the file system does not occupy the entire partition.
770 Furthermore we don't need the verity hash tree at the end of the root
771 file system because postinst will recreate it. This function reads the
772 (approximate) superblock of the ext4 partition and extracts the actual
773 file system size in the root partition.
Alex Klein1699fab2022-09-08 08:46:06 -0600774 """
775 superblock_size = 4096 * 2
776 with open(self._image, "rb") as r:
777 r.seek(offset)
778 with tempfile.NamedTemporaryFile(delete=False) as fp:
779 fp.write(r.read(superblock_size))
780 fp.close()
781 return offset, partition_lib.Ext2FileSystemSize(fp.name)
Amin Hassani75c5f942021-02-20 23:56:53 -0800782
Alex Klein1699fab2022-09-08 08:46:06 -0600783 def _RunPostInst(self, on_target: bool = True):
784 """Runs the postinst process in the root partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800785
Alex Klein1699fab2022-09-08 08:46:06 -0600786 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600787 on_target: If true the postinst is run on the target (inactive)
788 partition. This is used when doing normal updates. If false, the
789 postinst is run on the current (active) partition. This is used
790 when reverting an update.
Alex Klein1699fab2022-09-08 08:46:06 -0600791 """
792 try:
793 postinst_dir = "/"
794 partition = self._current_root
795 if on_target:
796 postinst_dir = self._device.run(
797 ["mktemp", "-d", "-p", self._device.work_dir],
798 capture_output=True,
799 ).stdout.strip()
800 self._device.run(
801 ["mount", "-o", "ro", self._target, postinst_dir]
802 )
803 partition = self._target
Amin Hassani75c5f942021-02-20 23:56:53 -0800804
Alex Klein1699fab2022-09-08 08:46:06 -0600805 self._ran_postinst = True
806 postinst = os.path.join(postinst_dir, "postinst")
807 result = self._device.run(
808 [postinst, partition], capture_output=True
809 )
Amin Hassani75c5f942021-02-20 23:56:53 -0800810
Alex Klein1699fab2022-09-08 08:46:06 -0600811 logging.debug(
812 "Postinst result on %s: \n%s", postinst, result.stdout
813 )
814 # DeviceImagerOperation will look for this log.
815 logging.info("Postinstall completed.")
816 finally:
817 if on_target:
818 self._device.run(["umount", postinst_dir])
Amin Hassani75c5f942021-02-20 23:56:53 -0800819
Alex Klein1699fab2022-09-08 08:46:06 -0600820 def Revert(self):
821 """Reverts the root update install."""
822 logging.info("Reverting the rootfs partition update.")
823 if self._ran_postinst:
Alex Klein975e86c2023-01-23 16:49:10 -0700824 # We don't have to do anything for revert if we haven't changed the
825 # kernel priorities yet.
Alex Klein1699fab2022-09-08 08:46:06 -0600826 self._RunPostInst(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800827
828
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000829class MiniOSUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600830 """A class to update the miniOS partition on a Chromium OS device."""
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000831
Alex Klein1699fab2022-09-08 08:46:06 -0600832 def __init__(self, *args):
833 """Initializes the class.
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000834
Alex Klein1699fab2022-09-08 08:46:06 -0600835 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600836 *args: See PartitionUpdaterBase
Alex Klein1699fab2022-09-08 08:46:06 -0600837 """
838 super().__init__(*args)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000839
Alex Klein1699fab2022-09-08 08:46:06 -0600840 self._ran_postinst = False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000841
Alex Klein1699fab2022-09-08 08:46:06 -0600842 def _GetPartitionName(self):
843 """See RawPartitionUpdater._GetPartitionName()."""
844 return constants.PART_MINIOS_A
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000845
Alex Klein1699fab2022-09-08 08:46:06 -0600846 def _GetRemotePartitionName(self):
847 """See RawPartitionUpdater._GetRemotePartitionName()."""
848 return constants.QUICK_PROVISION_PAYLOAD_MINIOS
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000849
Alex Klein1699fab2022-09-08 08:46:06 -0600850 def _Run(self):
851 """The function that does the job of rootfs partition update."""
852 if self._image_type == ImageType.FULL:
853 if self._MiniOSPartitionsExistInImage():
854 logging.info("Updating miniOS partition from local.")
855 super()._Run()
856 else:
857 logging.warning(
858 "Not updating miniOS partition as it does not exist."
859 )
860 return
861 elif self._image_type == ImageType.REMOTE_DIRECTORY:
862 if not gs.GSContext().Exists(
863 os.path.join(
864 self._image, constants.QUICK_PROVISION_PAYLOAD_MINIOS
865 )
866 ):
867 logging.warning("Not updating miniOS, missing remote files.")
868 return
869 elif not self._MiniOSPartitionsExist():
870 logging.warning("Not updating miniOS, missing partitions.")
871 return
872 else:
873 logging.info("Updating miniOS partition from remote.")
874 super()._Run()
875 else:
876 # Let super() handle this error.
877 super()._Run()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000878
Alex Klein1699fab2022-09-08 08:46:06 -0600879 self._RunPostInstall()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000880
Alex Klein1699fab2022-09-08 08:46:06 -0600881 def _RunPostInstall(self):
882 """The function will change the priority of the miniOS partitions."""
883 self._FlipMiniOSPriority()
884 self._ran_postinst = True
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000885
Alex Klein1699fab2022-09-08 08:46:06 -0600886 def Revert(self):
887 """Reverts the miniOS partition update."""
888 if self._ran_postinst:
889 self._FlipMiniOSPriority()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000890
Alex Klein1699fab2022-09-08 08:46:06 -0600891 def _GetMiniOSPriority(self):
892 return self._device.run(
893 ["crossystem", constants.MINIOS_PRIORITY]
894 ).stdout
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000895
Alex Klein1699fab2022-09-08 08:46:06 -0600896 def _SetMiniOSPriority(self, priority: str):
897 self._device.run(
898 ["crossystem", f"{constants.MINIOS_PRIORITY}={priority}"]
899 )
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000900
Alex Klein1699fab2022-09-08 08:46:06 -0600901 def _FlipMiniOSPriority(self):
902 inactive_minios_priority = (
903 "B" if self._GetMiniOSPriority() == "A" else "A"
904 )
905 logging.info("Setting miniOS priority to %s", inactive_minios_priority)
906 self._SetMiniOSPriority(inactive_minios_priority)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000907
Alex Klein1699fab2022-09-08 08:46:06 -0600908 def _MiniOSPartitionsExistInImage(self):
909 """Checks if miniOS partition exists in the image."""
910 d = cgpt.Disk.FromImage(self._image)
911 try:
912 d.GetPartitionByTypeGuid(cgpt.MINIOS_TYPE_GUID)
913 return True
914 except KeyError:
915 return False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000916
Alex Klein1699fab2022-09-08 08:46:06 -0600917 def _MiniOSPartitionsExist(self):
918 """Checks if the device has miniOS partitions."""
919 run = lambda x: self._device.run(x).stdout.strip()
920 device_drive = run(["rootdev", "-s", "-d"])
921 cmd = ["cgpt", "show", "-t", device_drive, "-i"]
922 return all(
923 (run(cmd + [p]) == cgpt.MINIOS_TYPE_GUID) for p in ("9", "10")
924 )
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700925
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000926
Amin Hassani74403082021-02-22 11:40:09 -0800927class StatefulPayloadGenerator(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600928 """A class for generating a stateful update payload in a separate thread."""
Amin Hassani74403082021-02-22 11:40:09 -0800929
Alex Klein1699fab2022-09-08 08:46:06 -0600930 def __init__(self, image: str):
931 """Initializes that class.
Amin Hassani74403082021-02-22 11:40:09 -0800932
Alex Klein1699fab2022-09-08 08:46:06 -0600933 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600934 image: The path to a local Chromium OS image.
Alex Klein1699fab2022-09-08 08:46:06 -0600935 """
936 super().__init__()
937 self._image = image
938
939 def run(self):
940 """Generates the stateful update and writes it into the output pipe."""
941 try:
942 paygen_stateful_payload_lib.GenerateStatefulPayload(
943 self._image, self._Source()
944 )
945 finally:
946 self._CloseSource()
Amin Hassani74403082021-02-22 11:40:09 -0800947
948
949class StatefulUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600950 """A class to update the stateful partition on a device."""
Amin Hassani74403082021-02-22 11:40:09 -0800951
Alex Klein1699fab2022-09-08 08:46:06 -0600952 def __init__(self, clobber_stateful: bool, *args):
953 """Initializes the class
Amin Hassani74403082021-02-22 11:40:09 -0800954
Alex Klein1699fab2022-09-08 08:46:06 -0600955 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600956 clobber_stateful: Whether to clobber the stateful or not.
957 *args: Look at PartitionUpdaterBase.
Alex Klein1699fab2022-09-08 08:46:06 -0600958 """
959 super().__init__(*args)
960 self._clobber_stateful = clobber_stateful
Amin Hassani74403082021-02-22 11:40:09 -0800961
Alex Klein1699fab2022-09-08 08:46:06 -0600962 def _Run(self):
Alex Klein975e86c2023-01-23 16:49:10 -0700963 """Read/Download the stateful updates and write it into the device."""
Alex Klein1699fab2022-09-08 08:46:06 -0600964 if self._image_type == ImageType.FULL:
965 generator_cls = StatefulPayloadGenerator
966 elif self._image_type == ImageType.REMOTE_DIRECTORY:
967 generator_cls = GsFileCopier
968 self._image = os.path.join(
969 self._image, paygen_stateful_payload_lib.STATEFUL_FILE
970 )
971 else:
972 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassani74403082021-02-22 11:40:09 -0800973
Alex Klein1699fab2022-09-08 08:46:06 -0600974 with generator_cls(self._image) as generator:
975 try:
976 updater = stateful_updater.StatefulUpdater(self._device)
977 updater.Update(
978 generator.Target(),
979 is_payload_on_device=False,
980 update_type=(
981 stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER
982 if self._clobber_stateful
983 else None
984 ),
985 )
986 finally:
987 generator.CloseTarget()
988
989 def Revert(self):
990 """Reverts the stateful partition update."""
991 logging.info("Reverting the stateful update.")
992 stateful_updater.StatefulUpdater(self._device).Reset()
Amin Hassani55970562021-02-22 20:49:13 -0800993
994
995class ProgressWatcher(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600996 """A class used for watching the progress of rootfs update."""
Amin Hassani55970562021-02-22 20:49:13 -0800997
Alex Klein1699fab2022-09-08 08:46:06 -0600998 def __init__(self, device, target_root: str):
999 """Initializes the class.
Amin Hassani55970562021-02-22 20:49:13 -08001000
Alex Klein1699fab2022-09-08 08:46:06 -06001001 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001002 device: The ChromiumOSDevice to be updated.
1003 target_root: The target root partition to monitor the progress of.
Alex Klein1699fab2022-09-08 08:46:06 -06001004 """
1005 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -08001006
Alex Klein1699fab2022-09-08 08:46:06 -06001007 self._device = device
1008 self._target_root = target_root
1009 self._exit = False
Amin Hassani55970562021-02-22 20:49:13 -08001010
Alex Klein1699fab2022-09-08 08:46:06 -06001011 def __enter__(self):
1012 """Starts the thread."""
1013 self.start()
1014 return self
Amin Hassani55970562021-02-22 20:49:13 -08001015
Alex Klein1699fab2022-09-08 08:46:06 -06001016 def __exit__(self, *args, **kwargs):
1017 """Exists the thread."""
1018 self._exit = True
1019 self.join()
Amin Hassani55970562021-02-22 20:49:13 -08001020
Alex Klein1699fab2022-09-08 08:46:06 -06001021 def _ShouldExit(self):
1022 return self._exit
Amin Hassani55970562021-02-22 20:49:13 -08001023
Alex Klein1699fab2022-09-08 08:46:06 -06001024 def run(self):
1025 """Monitors the progress of the target root partitions' update.
Amin Hassani55970562021-02-22 20:49:13 -08001026
Alex Klein975e86c2023-01-23 16:49:10 -07001027 This is done by periodically, reading the fd position of the process
1028 that is writing into the target partition and reporting it back. Then
1029 the position is divided by the size of the block device to report
1030 approximate progress.
Alex Klein1699fab2022-09-08 08:46:06 -06001031 """
1032 cmd = ["blockdev", "--getsize64", self._target_root]
Amin Hassani55970562021-02-22 20:49:13 -08001033 output = self._device.run(cmd, capture_output=True).stdout.strip()
Alex Klein1699fab2022-09-08 08:46:06 -06001034 if output is None:
1035 raise Error(f"Cannot get the block device size from {output}.")
1036 dev_size = int(output)
1037
1038 # Using lsof to find out which process is writing to the target rootfs.
1039 cmd = ["lsof", "-t", self._target_root]
Brian Norris7ceb0fe2022-11-10 17:46:31 -08001040 while True:
1041 if self._ShouldExit():
1042 return
1043
Alex Klein1699fab2022-09-08 08:46:06 -06001044 try:
1045 pid = self._device.run(cmd, capture_output=True).stdout.strip()
1046 if pid:
1047 break
1048 except cros_build_lib.RunCommandError:
1049 continue
1050 finally:
1051 time.sleep(1)
1052
Alex Klein975e86c2023-01-23 16:49:10 -07001053 # Now that we know which process is writing to it, we can look the
1054 # fdinfo of stdout of that process to get its offset. We're assuming
1055 # there will be no seek, which is correct.
Alex Klein1699fab2022-09-08 08:46:06 -06001056 cmd = ["cat", f"/proc/{pid}/fdinfo/1"]
1057 while not self._ShouldExit():
1058 try:
1059 output = self._device.run(
1060 cmd, capture_output=True
1061 ).stdout.strip()
1062 m = re.search(r"^pos:\s*(\d+)$", output, flags=re.M)
1063 if m:
1064 offset = int(m.group(1))
1065 # DeviceImagerOperation will look for this log.
1066 logging.info("RootFS progress: %f", offset / dev_size)
1067 except cros_build_lib.RunCommandError:
1068 continue
1069 finally:
1070 time.sleep(1)
Amin Hassani55970562021-02-22 20:49:13 -08001071
1072
1073class DeviceImagerOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -06001074 """A class to provide a progress bar for DeviceImager operation."""
Amin Hassani55970562021-02-22 20:49:13 -08001075
Alex Klein1699fab2022-09-08 08:46:06 -06001076 def __init__(self):
1077 """Initializes the class."""
1078 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -08001079
Alex Klein1699fab2022-09-08 08:46:06 -06001080 self._progress = 0.0
Amin Hassani55970562021-02-22 20:49:13 -08001081
Alex Klein1699fab2022-09-08 08:46:06 -06001082 def ParseOutput(self, output=None):
1083 """Override function to parse the output and provide progress.
Amin Hassani55970562021-02-22 20:49:13 -08001084
Alex Klein1699fab2022-09-08 08:46:06 -06001085 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001086 output: The stderr or stdout.
Alex Klein1699fab2022-09-08 08:46:06 -06001087 """
1088 output = self._stdout.read()
1089 match = re.findall(r"RootFS progress: (\d+(?:\.\d+)?)", output)
1090 if match:
1091 progress = float(match[0])
1092 self._progress = max(self._progress, progress)
Amin Hassani55970562021-02-22 20:49:13 -08001093
Alex Klein1699fab2022-09-08 08:46:06 -06001094 # If postinstall completes, move half of the remaining progress.
1095 if re.findall(r"Postinstall completed", output):
1096 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001097
Alex Klein975e86c2023-01-23 16:49:10 -07001098 # While waiting for reboot, each time, move half of the remaining
1099 # progress.
Alex Klein1699fab2022-09-08 08:46:06 -06001100 if re.findall(r"Unable to get new boot_id", output):
1101 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001102
Alex Klein1699fab2022-09-08 08:46:06 -06001103 if re.findall(r"DeviceImager completed.", output):
1104 self._progress = 1.0
Amin Hassani55970562021-02-22 20:49:13 -08001105
Alex Klein1699fab2022-09-08 08:46:06 -06001106 self.ProgressBar(self._progress)