blob: 2a61e37a0de2b83c9a35f71ab9c5ec38895b4667 [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
Amin Hassanid4b3ff82021-02-20 23:05:14 -080014import tempfile
15import threading
Amin Hassani55970562021-02-22 20:49:13 -080016import time
Daichi Hironoc1a8fd32022-01-07 22:17:51 +090017from typing import Dict, List, Tuple, Union
Amin Hassani92f6c4a2021-02-20 17:36:09 -080018
Amin Hassani55970562021-02-22 20:49:13 -080019from chromite.cli import command
Amin Hassanicf8f0042021-03-12 10:42:13 -080020from chromite.cli import flash
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000021from chromite.lib import cgpt
Amin Hassanid4b3ff82021-02-20 23:05:14 -080022from chromite.lib import constants
Amin Hassani92f6c4a2021-02-20 17:36:09 -080023from chromite.lib import cros_build_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080024from chromite.lib import gs
Amin Hassanid4b3ff82021-02-20 23:05:14 -080025from chromite.lib import image_lib
Amin Hassani55970562021-02-22 20:49:13 -080026from chromite.lib import operation
Amin Hassanid4b3ff82021-02-20 23:05:14 -080027from chromite.lib import osutils
Amin Hassani92f6c4a2021-02-20 17:36:09 -080028from chromite.lib import parallel
29from chromite.lib import remote_access
30from chromite.lib import retry_util
Amin Hassani74403082021-02-22 11:40:09 -080031from chromite.lib import stateful_updater
Amin Hassani75c5f942021-02-20 23:56:53 -080032from chromite.lib.paygen import partition_lib
Amin Hassani74403082021-02-22 11:40:09 -080033from chromite.lib.paygen import paygen_stateful_payload_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080034from chromite.lib.xbuddy import devserver_constants
35from chromite.lib.xbuddy import xbuddy
Alex Klein18ef1212021-10-14 12:49:02 -060036from chromite.utils import timer
Amin Hassani92f6c4a2021-02-20 17:36:09 -080037
38
Amin Hassani92f6c4a2021-02-20 17:36:09 -080039class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060040 """Thrown when there is a general Chromium OS-specific flash error."""
Amin Hassani92f6c4a2021-02-20 17:36:09 -080041
42
43class ImageType(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060044 """Type of the image that is used for flashing the device."""
Amin Hassani92f6c4a2021-02-20 17:36:09 -080045
Alex Klein1699fab2022-09-08 08:46:06 -060046 # The full image on disk (e.g. chromiumos_test_image.bin).
47 FULL = 0
48 # The remote directory path
49 # (e.g gs://chromeos-image-archive/eve-release/R90-x.x.x)
50 REMOTE_DIRECTORY = 1
Amin Hassani92f6c4a2021-02-20 17:36:09 -080051
52
53class Partition(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060054 """An enum for partition types like kernel and rootfs."""
55
56 KERNEL = 0
57 ROOTFS = 1
58 MINIOS = 2
Amin Hassani92f6c4a2021-02-20 17:36:09 -080059
60
61class DeviceImager(object):
Alex Klein1699fab2022-09-08 08:46:06 -060062 """A class to flash a Chromium OS device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080063
Alex Klein975e86c2023-01-23 16:49:10 -070064 This utility uses parallelism as much as possible to achieve its goal as
65 fast as possible. For example, it uses parallel compressors, parallel
66 transfers, and simultaneous pipes.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080067 """
68
Alex Klein1699fab2022-09-08 08:46:06 -060069 # The parameters of the kernel and rootfs's two main partitions.
70 A = {Partition.KERNEL: 2, Partition.ROOTFS: 3}
71 B = {Partition.KERNEL: 4, Partition.ROOTFS: 5}
Amin Hassani92f6c4a2021-02-20 17:36:09 -080072
Alex Klein1699fab2022-09-08 08:46:06 -060073 MINIOS_A = {Partition.MINIOS: 9}
74 MINIOS_B = {Partition.MINIOS: 10}
Amin Hassani92f6c4a2021-02-20 17:36:09 -080075
Alex Klein1699fab2022-09-08 08:46:06 -060076 def __init__(
77 self,
78 device,
79 image: str,
80 board: str = None,
81 version: str = None,
82 no_rootfs_update: bool = False,
83 no_stateful_update: bool = False,
84 no_minios_update: bool = False,
85 no_reboot: bool = False,
86 disable_verification: bool = False,
87 clobber_stateful: bool = False,
88 clear_tpm_owner: bool = False,
89 delta: bool = False,
Jae Hoon Kim04a08062023-03-15 06:06:03 +000090 reboot_timeout: datetime.timedelta = None,
Alex Klein1699fab2022-09-08 08:46:06 -060091 ):
92 """Initialize DeviceImager for flashing a Chromium OS device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080093
Alex Klein1699fab2022-09-08 08:46:06 -060094 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -060095 device: The ChromiumOSDevice to be updated.
96 image: The target image path (can be xBuddy path).
97 board: Board to use.
98 version: Image version to use.
99 no_rootfs_update: Whether to do rootfs partition update.
100 no_stateful_update: Whether to do stateful partition update.
101 no_minios_update: Whether to do minios partition update.
102 no_reboot: Whether to reboot device after update, default True.
103 disable_verification: Whether to disable rootfs verification on the
104 device.
105 clobber_stateful: Whether to do a clean stateful partition.
106 clear_tpm_owner: If true, it will clear the TPM owner on reboot.
107 delta: Whether to use delta compression when transferring image
108 bytes.
Jae Hoon Kim04a08062023-03-15 06:06:03 +0000109 reboot_timeout: The timeout for reboot.
Alex Klein1699fab2022-09-08 08:46:06 -0600110 """
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800111
Alex Klein1699fab2022-09-08 08:46:06 -0600112 self._device = device
113 self._image = image
114 self._board = board
115 self._version = version
116 self._no_rootfs_update = no_rootfs_update
117 self._no_stateful_update = no_stateful_update
118 self._no_minios_update = no_minios_update
119 self._no_reboot = no_reboot
120 self._disable_verification = disable_verification
121 self._clobber_stateful = clobber_stateful
122 self._clear_tpm_owner = clear_tpm_owner
Jae Hoon Kim04a08062023-03-15 06:06:03 +0000123 self._reboot_timeout = reboot_timeout
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800124
Alex Klein1699fab2022-09-08 08:46:06 -0600125 self._image_type = None
126 self._inactive_state = None
127 self._delta = delta
Daichi Hirono28831b3b2022-04-07 12:41:11 +0900128
Alex Klein1699fab2022-09-08 08:46:06 -0600129 def Run(self):
130 """Update the device with image of specific version."""
131 self._LocateImage()
132 logging.notice(
133 "Preparing to update the remote device %s with image %s",
134 self._device.hostname,
135 self._image,
136 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800137
Alex Klein1699fab2022-09-08 08:46:06 -0600138 try:
139 if command.UseProgressBar():
140 op = DeviceImagerOperation()
141 op.Run(self._Run)
142 else:
143 self._Run()
144 except Exception as e:
145 raise Error(f"DeviceImager Failed with error: {e}")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800146
Alex Klein1699fab2022-09-08 08:46:06 -0600147 # DeviceImagerOperation will look for this log.
148 logging.info("DeviceImager completed.")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800149
Alex Klein1699fab2022-09-08 08:46:06 -0600150 def _Run(self):
151 """Runs the various operations to install the image on device."""
152 # TODO(b/228389041): Switch to delta compression if self._delta is True
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800153
Alex Klein1699fab2022-09-08 08:46:06 -0600154 self._InstallPartitions()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800155
Alex Klein1699fab2022-09-08 08:46:06 -0600156 if self._clear_tpm_owner:
157 self._device.ClearTpmOwner()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800158
Daichi Hirono1d45ed52023-01-20 17:30:26 +0900159 if self._disable_verification:
160 # DisableRootfsVerification internally invokes Reboot().
Jae Hoon Kim04a08062023-03-15 06:06:03 +0000161 self._device.DisableRootfsVerification(
162 timeout_sec=self._reboot_timeout.total_seconds()
163 )
Daichi Hirono1d45ed52023-01-20 17:30:26 +0900164 self._VerifyBootExpectations()
165 elif not self._no_reboot:
Alex Klein1699fab2022-09-08 08:46:06 -0600166 self._Reboot()
167 self._VerifyBootExpectations()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800168
Alex Klein1699fab2022-09-08 08:46:06 -0600169 def _LocateImage(self):
170 """Locates the path to the final image(s) that need to be installed.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800171
Alex Klein1699fab2022-09-08 08:46:06 -0600172 If the paths is local, the image should be the Chromium OS GPT image
Alex Klein975e86c2023-01-23 16:49:10 -0700173 (e.g. chromiumos_test_image.bin). If the path is remote, it should be
174 the remote directory where we can find the quick-provision and stateful
175 update files (e.g. gs://chromeos-image-archive/eve-release/R90-x.x.x).
Amin Hassanicf8f0042021-03-12 10:42:13 -0800176
Alex Klein975e86c2023-01-23 16:49:10 -0700177 NOTE: At this point there is no caching involved. Hence we always
178 download the partition payloads or extract them from the Chromium OS
179 image.
Alex Klein1699fab2022-09-08 08:46:06 -0600180 """
181 if os.path.isfile(self._image):
182 self._image_type = ImageType.FULL
183 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800184
Alex Klein1699fab2022-09-08 08:46:06 -0600185 # TODO(b/172212406): We could potentially also allow this by searching
Alex Klein975e86c2023-01-23 16:49:10 -0700186 # through the directory to see whether we have quick-provision and
187 # stateful payloads. This only makes sense when a user has their
188 # workstation at home and doesn't want to incur the bandwidth cost of
189 # downloading the same image multiple times. For that, they can simply
190 # download the GPT image image first and flash that instead.
Alex Klein1699fab2022-09-08 08:46:06 -0600191 if os.path.isdir(self._image):
192 raise ValueError(
193 f"{self._image}: input must be a disk image, not a directory."
194 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800195
Alex Klein1699fab2022-09-08 08:46:06 -0600196 if gs.PathIsGs(self._image):
197 # TODO(b/172212406): Check whether it is a directory. If it wasn't a
Alex Klein975e86c2023-01-23 16:49:10 -0700198 # directory download the image into some temp location and use it
199 # instead.
Alex Klein1699fab2022-09-08 08:46:06 -0600200 self._image_type = ImageType.REMOTE_DIRECTORY
201 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800202
Alex Klein1699fab2022-09-08 08:46:06 -0600203 # Assuming it is an xBuddy path.
204 board = cros_build_lib.GetBoard(
205 device_board=self._device.board or flash.GetDefaultBoard(),
206 override_board=self._board,
207 force=True,
208 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800209
Alex Klein1699fab2022-09-08 08:46:06 -0600210 xb = xbuddy.XBuddy(board=board, version=self._version)
211 build_id, local_file = xb.Translate([self._image])
212 if build_id is None:
213 raise Error(f"{self._image}: unable to find matching xBuddy path.")
214 logging.info("XBuddy path translated to build ID %s", build_id)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800215
Alex Klein1699fab2022-09-08 08:46:06 -0600216 if local_file:
217 self._image = local_file
218 self._image_type = ImageType.FULL
219 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800220
Alex Klein1699fab2022-09-08 08:46:06 -0600221 self._image = f"{devserver_constants.GS_IMAGE_DIR}/{build_id}"
222 self._image_type = ImageType.REMOTE_DIRECTORY
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800223
Alex Klein1699fab2022-09-08 08:46:06 -0600224 def _SplitDevPath(self, path: str) -> Tuple[str, int]:
225 """Splits the given /dev/x path into prefix and the dev number.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800226
Alex Klein1699fab2022-09-08 08:46:06 -0600227 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600228 path: The path to a block dev device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800229
Alex Klein1699fab2022-09-08 08:46:06 -0600230 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600231 A tuple of representing the prefix and the index of the dev path.
232 e.g.: '/dev/mmcblk0p1' -> ['/dev/mmcblk0p', 1]
Alex Klein1699fab2022-09-08 08:46:06 -0600233 """
234 match = re.search(r"(.*)([0-9]+)$", path)
235 if match is None:
236 raise Error(f"{path}: Could not parse root dev path.")
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000237
Alex Klein1699fab2022-09-08 08:46:06 -0600238 return match.group(1), int(match.group(2))
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000239
Alex Klein1699fab2022-09-08 08:46:06 -0600240 def _GetKernelState(self, root_num: int) -> Tuple[Dict, Dict]:
241 """Returns the kernel state.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800242
Alex Klein1699fab2022-09-08 08:46:06 -0600243 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600244 A tuple of two dictionaries: The current active kernel state and the
245 inactive kernel state. (Look at A and B constants in this class.)
Alex Klein1699fab2022-09-08 08:46:06 -0600246 """
247 if root_num == self.A[Partition.ROOTFS]:
248 return self.A, self.B
249 elif root_num == self.B[Partition.ROOTFS]:
250 return self.B, self.A
251 else:
252 raise Error(f"Invalid root partition number {root_num}")
Amin Hassanid684e982021-02-26 11:10:58 -0800253
Alex Klein1699fab2022-09-08 08:46:06 -0600254 def _GetMiniOSState(self, minios_num: int) -> Tuple[Dict, Dict]:
255 """Returns the miniOS state.
Amin Hassani75c5f942021-02-20 23:56:53 -0800256
Alex Klein1699fab2022-09-08 08:46:06 -0600257 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600258 A tuple of dictionaries: The current active miniOS state and the
259 inactive miniOS state.
Alex Klein1699fab2022-09-08 08:46:06 -0600260 """
261 if minios_num == self.MINIOS_A[Partition.MINIOS]:
262 return self.MINIOS_A, self.MINIOS_B
263 elif minios_num == self.MINIOS_B[Partition.MINIOS]:
264 return self.MINIOS_B, self.MINIOS_A
265 else:
266 raise Error(f"Invalid minios partition number {minios_num}")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800267
Alex Klein1699fab2022-09-08 08:46:06 -0600268 def _InstallPartitions(self):
269 """The main method that installs the partitions of a Chrome OS device.
Amin Hassani74403082021-02-22 11:40:09 -0800270
Alex Klein1699fab2022-09-08 08:46:06 -0600271 It uses parallelism to install the partitions as fast as possible.
272 """
273 prefix, root_num = self._SplitDevPath(self._device.root_dev)
274 active_state, self._inactive_state = self._GetKernelState(root_num)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000275
Alex Klein1699fab2022-09-08 08:46:06 -0600276 updaters = []
277 if not self._no_rootfs_update:
278 current_root = prefix + str(active_state[Partition.ROOTFS])
279 target_root = prefix + str(self._inactive_state[Partition.ROOTFS])
280 updaters.append(
281 RootfsUpdater(
282 current_root,
283 self._device,
284 self._image,
285 self._image_type,
286 target_root,
287 )
288 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800289
Alex Klein1699fab2022-09-08 08:46:06 -0600290 target_kernel = prefix + str(self._inactive_state[Partition.KERNEL])
291 updaters.append(
292 KernelUpdater(
293 self._device, self._image, self._image_type, target_kernel
294 )
295 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800296
Alex Klein1699fab2022-09-08 08:46:06 -0600297 if not self._no_stateful_update:
298 updaters.append(
299 StatefulUpdater(
300 self._clobber_stateful,
301 self._device,
302 self._image,
303 self._image_type,
304 None,
305 )
306 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800307
Alex Klein1699fab2022-09-08 08:46:06 -0600308 if not self._no_minios_update:
309 minios_priority = self._device.run(
310 ["crossystem", constants.MINIOS_PRIORITY]
311 ).stdout
312 if minios_priority not in ["A", "B"]:
313 logging.warning(
314 "Skipping miniOS flash due to missing priority."
315 )
316 else:
317 # Reference disk_layout_v3 for partition numbering.
318 _, inactive_minios_state = self._GetMiniOSState(
319 9 if minios_priority == "A" else 10
320 )
321 target_minios = prefix + str(
322 inactive_minios_state[Partition.MINIOS]
323 )
324 minios_updater = MiniOSUpdater(
325 self._device, self._image, self._image_type, target_minios
326 )
327 updaters.append(minios_updater)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800328
Alex Klein975e86c2023-01-23 16:49:10 -0700329 # Retry the partitions updates that failed, in case a transient error
330 # (like SSH drop, etc) caused the error.
Alex Klein1699fab2022-09-08 08:46:06 -0600331 num_retries = 1
332 try:
333 retry_util.RetryException(
334 Error,
335 num_retries,
336 parallel.RunParallelSteps,
337 (x.Run for x in updaters if not x.IsFinished()),
338 halt_on_error=True,
339 )
340 except Exception:
Alex Klein975e86c2023-01-23 16:49:10 -0700341 # If one of the partitions failed to be installed, revert all
342 # partitions.
Alex Klein1699fab2022-09-08 08:46:06 -0600343 parallel.RunParallelSteps(x.Revert for x in updaters)
344 raise
345
346 def _Reboot(self):
347 """Reboots the device."""
348 try:
Jae Hoon Kim04a08062023-03-15 06:06:03 +0000349 self._device.Reboot(
350 timeout_sec=self._reboot_timeout.total_seconds()
351 )
Alex Klein1699fab2022-09-08 08:46:06 -0600352 except remote_access.RebootError:
353 raise Error(
354 "Could not recover from reboot. Once example reason"
355 " could be the image provided was a non-test image"
356 " or the system failed to boot after the update."
357 )
358 except Exception as e:
359 raise Error(f"Failed to reboot to the device with error: {e}")
360
361 def _VerifyBootExpectations(self):
362 """Verify that we fully booted into the expected kernel state."""
363 # Discover the newly active kernel.
364 _, root_num = self._SplitDevPath(self._device.root_dev)
365 active_state, _ = self._GetKernelState(root_num)
366
367 # If this happens, we should rollback.
368 if active_state != self._inactive_state:
369 raise Error("The expected kernel state after update is invalid.")
370
371 logging.info("Verified boot expectations.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800372
373
374class ReaderBase(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600375 """The base class for reading different inputs and writing into output.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800376
Alex Klein975e86c2023-01-23 16:49:10 -0700377 This class extends threading.Thread, so it will be run on its own thread.
378 Also it can be used as a context manager. Internally, it opens necessary
379 files for writing to and reading from. This class cannot be instantiated, it
380 needs to be sub-classed first to provide necessary function implementations.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800381 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800382
Alex Klein1699fab2022-09-08 08:46:06 -0600383 def __init__(self, use_named_pipes: bool = False):
384 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800385
Alex Klein1699fab2022-09-08 08:46:06 -0600386 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600387 use_named_pipes: Whether to use a named pipe or anonymous file
Alex Klein1699fab2022-09-08 08:46:06 -0600388 descriptors.
389 """
390 super().__init__()
391 self._use_named_pipes = use_named_pipes
392 self._pipe_target = None
393 self._pipe_source = None
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800394
Alex Klein1699fab2022-09-08 08:46:06 -0600395 def __del__(self):
396 """Destructor.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800397
Alex Klein1699fab2022-09-08 08:46:06 -0600398 Make sure to clean up any named pipes we might have created.
399 """
400 if self._use_named_pipes:
401 osutils.SafeUnlink(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800402
Alex Klein1699fab2022-09-08 08:46:06 -0600403 def __enter__(self):
404 """Enters the context manager"""
405 if self._use_named_pipes:
Alex Klein975e86c2023-01-23 16:49:10 -0700406 # There is no need for the temp file, we only need its path. So the
407 # named pipe is created after this temp file is deleted.
Alex Klein1699fab2022-09-08 08:46:06 -0600408 with tempfile.NamedTemporaryFile(
409 prefix="chromite-device-imager"
410 ) as fp:
411 self._pipe_target = self._pipe_source = fp.name
412 os.mkfifo(self._pipe_target)
413 else:
414 self._pipe_target, self._pipe_source = os.pipe()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800415
Alex Klein1699fab2022-09-08 08:46:06 -0600416 self.start()
417 return self
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800418
Alex Klein1699fab2022-09-08 08:46:06 -0600419 def __exit__(self, *args, **kwargs):
420 """Exits the context manager."""
421 self.join()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800422
Alex Klein1699fab2022-09-08 08:46:06 -0600423 def _Source(self):
424 """Returns the source pipe to write data into.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800425
Alex Klein1699fab2022-09-08 08:46:06 -0600426 Sub-classes can use this function to determine where to write their data
427 into.
428 """
429 return self._pipe_source
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800430
Alex Klein1699fab2022-09-08 08:46:06 -0600431 def _CloseSource(self):
432 """Closes the source pipe.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800433
Alex Klein975e86c2023-01-23 16:49:10 -0700434 Sub-classes should use this function to close the pipe after they are
435 done writing into it. Failure to do so may result reader of the data to
436 hang indefinitely.
Alex Klein1699fab2022-09-08 08:46:06 -0600437 """
438 if not self._use_named_pipes:
439 os.close(self._pipe_source)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800440
Alex Klein1699fab2022-09-08 08:46:06 -0600441 def Target(self):
442 """Returns the target pipe to read data from.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800443
Alex Klein1699fab2022-09-08 08:46:06 -0600444 Users of this class can use this path to read data from.
445 """
446 return self._pipe_target
447
448 def CloseTarget(self):
449 """Closes the target pipe.
450
Alex Klein975e86c2023-01-23 16:49:10 -0700451 Users of this class should use this function to close the pipe after
452 they are done reading from it.
Alex Klein1699fab2022-09-08 08:46:06 -0600453 """
454 if self._use_named_pipes:
455 os.remove(self._pipe_target)
456 else:
457 os.close(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800458
459
460class PartialFileReader(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600461 """A class to read specific offset and length from a file and compress it.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800462
Alex Klein1699fab2022-09-08 08:46:06 -0600463 This class can be used to read from specific location and length in a file
Alex Klein975e86c2023-01-23 16:49:10 -0700464 (e.g. A partition in a GPT image). Then it compresses the input and writes
465 it out (to a pipe). Look at the base class for more information.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800466 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800467
Alex Klein1699fab2022-09-08 08:46:06 -0600468 # The offset of different partitions in a Chromium OS image does not always
Alex Klein975e86c2023-01-23 16:49:10 -0700469 # align to larger values like 4096. It seems that 512 is the maximum value
470 # to be divisible by partition offsets. This size should not be increased
471 # just for 'performance reasons'. Since we are doing everything in parallel,
472 # in practice there is not much difference between this and larger block
473 # sizes as parallelism hides the possible extra latency provided by smaller
474 # block sizes.
Alex Klein1699fab2022-09-08 08:46:06 -0600475 _BLOCK_SIZE = 512
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800476
Alex Klein1699fab2022-09-08 08:46:06 -0600477 def __init__(
478 self,
479 image: str,
480 offset: int,
481 length: int,
482 compression_command: List[str],
483 ):
484 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800485
Alex Klein1699fab2022-09-08 08:46:06 -0600486 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600487 image: The path to an image (local or remote directory).
488 offset: The offset (in bytes) to read from the image.
489 length: The length (in bytes) to read from the image.
490 compression_command: The command to compress transferred bytes.
Alex Klein1699fab2022-09-08 08:46:06 -0600491 """
492 super().__init__()
493
494 self._image = image
495 self._offset = offset
496 self._length = length
497 self._compression_command = compression_command
498
499 def run(self):
500 """Runs the reading and compression."""
Mike Frysinger906119e2022-12-27 18:10:23 -0500501 data = osutils.ReadFile(
502 self._image, mode="rb", size=self._length, seek=self._offset
503 )
Alex Klein1699fab2022-09-08 08:46:06 -0600504 try:
Mike Frysinger906119e2022-12-27 18:10:23 -0500505 cros_build_lib.run(
506 self._compression_command, input=data, stdout=self._Source()
507 )
Alex Klein1699fab2022-09-08 08:46:06 -0600508 finally:
509 self._CloseSource()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800510
511
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800512class GsFileCopier(ReaderBase):
Alex Klein975e86c2023-01-23 16:49:10 -0700513 """A class to download gzip compressed file from GS bucket into a pipe."""
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800514
Alex Klein1699fab2022-09-08 08:46:06 -0600515 def __init__(self, image: str):
516 """Initializes the class.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800517
Alex Klein1699fab2022-09-08 08:46:06 -0600518 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600519 image: The path to an image (local or remote directory).
Alex Klein1699fab2022-09-08 08:46:06 -0600520 """
521 super().__init__(use_named_pipes=True)
522 self._image = image
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800523
Alex Klein1699fab2022-09-08 08:46:06 -0600524 def run(self):
525 """Runs the download and write into the output pipe."""
526 try:
527 gs.GSContext().Copy(self._image, self._Source())
528 finally:
529 self._CloseSource()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800530
531
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800532class PartitionUpdaterBase(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600533 """A base abstract class to use for installing an image into a partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800534
Alex Klein1699fab2022-09-08 08:46:06 -0600535 Sub-classes should implement the abstract methods to provide the core
536 functionality.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800537 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800538
Alex Klein1699fab2022-09-08 08:46:06 -0600539 def __init__(self, device, image: str, image_type, target: str):
Alex Klein975e86c2023-01-23 16:49:10 -0700540 """Initializes this base class with the most commonly needed values.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800541
Alex Klein1699fab2022-09-08 08:46:06 -0600542 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600543 device: The ChromiumOSDevice to be updated.
544 image: The target image path for the partition update.
545 image_type: The type of the image (ImageType).
546 target: The target path (e.g. block dev) to install the update.
Alex Klein1699fab2022-09-08 08:46:06 -0600547 """
548 self._device = device
549 self._image = image
550 self._image_type = image_type
551 self._target = target
552 self._finished = False
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800553
Alex Klein1699fab2022-09-08 08:46:06 -0600554 def Run(self):
555 """The main function that does the partition update job."""
556 with timer.Timer() as t:
557 try:
558 self._Run()
559 finally:
560 self._finished = True
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800561
Alex Klein1699fab2022-09-08 08:46:06 -0600562 logging.debug("Completed %s in %s", self.__class__.__name__, t)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800563
Alex Klein1699fab2022-09-08 08:46:06 -0600564 @abc.abstractmethod
565 def _Run(self):
566 """The method that need to be implemented by sub-classes."""
567 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800568
Alex Klein1699fab2022-09-08 08:46:06 -0600569 def IsFinished(self):
570 """Returns whether the partition update has been successful."""
571 return self._finished
572
573 @abc.abstractmethod
574 def Revert(self):
575 """Reverts the partition update.
576
Alex Klein975e86c2023-01-23 16:49:10 -0700577 Subclasses need to implement this function to provide revert capability.
Alex Klein1699fab2022-09-08 08:46:06 -0600578 """
579 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800580
581
582class RawPartitionUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600583 """A class to update a raw partition on a Chromium OS device."""
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800584
Alex Klein1699fab2022-09-08 08:46:06 -0600585 def _Run(self):
586 """The function that does the job of kernel partition update."""
587 if self._image_type == ImageType.FULL:
588 self._CopyPartitionFromImage(self._GetPartitionName())
589 elif self._image_type == ImageType.REMOTE_DIRECTORY:
590 self._RedirectPartition(self._GetRemotePartitionName())
591 else:
592 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800593
Alex Klein1699fab2022-09-08 08:46:06 -0600594 def _GetPartitionName(self):
595 """Returns the name of the partition in a Chromium OS GPT layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800596
Alex Klein1699fab2022-09-08 08:46:06 -0600597 Subclasses should override this function to return correct name.
598 """
599 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800600
Alex Klein1699fab2022-09-08 08:46:06 -0600601 def _CopyPartitionFromImage(self, part_name: str):
602 """Updates the device's partition from a local Chromium OS image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800603
Alex Klein1699fab2022-09-08 08:46:06 -0600604 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600605 part_name: The name of the partition in the source image that needs
606 to be extracted.
Alex Klein1699fab2022-09-08 08:46:06 -0600607 """
608 offset, length = self._GetPartLocation(part_name)
609 offset, length = self._OptimizePartLocation(offset, length)
610 compressor, decompressor = self._GetCompressionAndDecompression()
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900611
Alex Klein1699fab2022-09-08 08:46:06 -0600612 with PartialFileReader(
613 self._image, offset, length, compressor
614 ) as generator:
615 try:
616 self._WriteToTarget(generator.Target(), decompressor)
617 finally:
618 generator.CloseTarget()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800619
Alex Klein1699fab2022-09-08 08:46:06 -0600620 def _GetCompressionAndDecompression(self) -> Tuple[List[str], List[str]]:
621 """Returns compression / decompression commands."""
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900622
Alex Klein1699fab2022-09-08 08:46:06 -0600623 return (
Mike Frysinger66306012022-04-22 15:23:13 -0400624 [
625 cros_build_lib.FindCompressor(
626 cros_build_lib.CompressionType.GZIP
627 )
628 ],
629 self._device.GetDecompressor(cros_build_lib.CompressionType.GZIP),
Alex Klein1699fab2022-09-08 08:46:06 -0600630 )
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900631
Alex Klein1699fab2022-09-08 08:46:06 -0600632 def _WriteToTarget(
633 self, source: Union[int, BytesIO], decompress_command: List[str]
634 ) -> None:
635 """Writes bytes source to the target device on DUT.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800636
Alex Klein1699fab2022-09-08 08:46:06 -0600637 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600638 A string command to run on a device to read data from stdin,
639 uncompress it and write it to the target partition.
Alex Klein1699fab2022-09-08 08:46:06 -0600640 """
Alex Klein1699fab2022-09-08 08:46:06 -0600641 cmd = " ".join(
642 [
643 *decompress_command,
644 "|",
645 "dd",
646 "bs=1M",
Alex Klein1699fab2022-09-08 08:46:06 -0600647 f"of={self._target}",
648 ]
649 )
650 self._device.run(cmd, input=source, shell=True)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800651
Alex Klein1699fab2022-09-08 08:46:06 -0600652 def _GetPartLocation(self, part_name: str):
653 """Extracts the location and size of the raw partition from the image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800654
Alex Klein1699fab2022-09-08 08:46:06 -0600655 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600656 part_name: The name of the partition in the source image that needs
657 to be extracted.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800658
Alex Klein1699fab2022-09-08 08:46:06 -0600659 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600660 A tuple of offset and length (in bytes) from the image.
Alex Klein1699fab2022-09-08 08:46:06 -0600661 """
662 try:
663 parts = image_lib.GetImageDiskPartitionInfo(self._image)
664 part_info = [p for p in parts if p.name == part_name][0]
665 except IndexError:
666 raise Error(f"No partition named {part_name} found.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800667
Alex Klein1699fab2022-09-08 08:46:06 -0600668 return int(part_info.start), int(part_info.size)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800669
Alex Klein1699fab2022-09-08 08:46:06 -0600670 def _GetRemotePartitionName(self):
671 """Returns the name of the quick-provision partition file.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800672
Alex Klein1699fab2022-09-08 08:46:06 -0600673 Subclasses should override this function to return correct name.
674 """
675 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800676
Alex Klein1699fab2022-09-08 08:46:06 -0600677 def _OptimizePartLocation(self, offset: int, length: int):
678 """Optimizes the offset and length of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800679
Alex Klein975e86c2023-01-23 16:49:10 -0700680 Subclasses can override this to provide better offset/length than what
681 is defined in the PGT partition layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800682
Alex Klein1699fab2022-09-08 08:46:06 -0600683 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600684 offset: The offset (in bytes) of the partition in the image.
685 length: The length (in bytes) of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800686
Alex Klein1699fab2022-09-08 08:46:06 -0600687 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600688 A tuple of offset and length (in bytes) from the image.
Alex Klein1699fab2022-09-08 08:46:06 -0600689 """
690 return offset, length
Amin Hassanid684e982021-02-26 11:10:58 -0800691
Alex Klein1699fab2022-09-08 08:46:06 -0600692 def _RedirectPartition(self, file_name: str):
693 """Downloads the partition from a remote path and writes it into target.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800694
Alex Klein1699fab2022-09-08 08:46:06 -0600695 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600696 file_name: The file name in the remote directory self._image.
Alex Klein1699fab2022-09-08 08:46:06 -0600697 """
698 image_path = os.path.join(self._image, file_name)
699 with GsFileCopier(image_path) as generator:
700 try:
701 with open(generator.Target(), "rb") as fp:
702 # Always use GZIP as remote quick provision images are gzip
703 # compressed only.
704 self._WriteToTarget(
705 fp,
Mike Frysinger66306012022-04-22 15:23:13 -0400706 self._device.GetDecompressor(
707 cros_build_lib.CompressionType.GZIP
708 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600709 )
710 finally:
711 generator.CloseTarget()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800712
Amin Hassanid684e982021-02-26 11:10:58 -0800713
714class KernelUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600715 """A class to update the kernel partition on a Chromium OS device."""
Amin Hassanid684e982021-02-26 11:10:58 -0800716
Alex Klein1699fab2022-09-08 08:46:06 -0600717 def _GetPartitionName(self):
718 """See RawPartitionUpdater._GetPartitionName()."""
719 return constants.PART_KERN_B
Amin Hassanid684e982021-02-26 11:10:58 -0800720
Alex Klein1699fab2022-09-08 08:46:06 -0600721 def _GetRemotePartitionName(self):
722 """See RawPartitionUpdater._GetRemotePartitionName()."""
723 return constants.QUICK_PROVISION_PAYLOAD_KERNEL
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800724
Alex Klein1699fab2022-09-08 08:46:06 -0600725 def Revert(self):
726 """Reverts the kernel partition update."""
727 # There is nothing to do for reverting kernel partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800728
729
730class RootfsUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600731 """A class to update the root partition on a Chromium OS device."""
Amin Hassani75c5f942021-02-20 23:56:53 -0800732
Alex Klein1699fab2022-09-08 08:46:06 -0600733 def __init__(self, current_root: str, *args):
734 """Initializes the class.
Amin Hassani75c5f942021-02-20 23:56:53 -0800735
Alex Klein1699fab2022-09-08 08:46:06 -0600736 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600737 current_root: The current root device path.
738 *args: See PartitionUpdaterBase
Alex Klein1699fab2022-09-08 08:46:06 -0600739 """
740 super().__init__(*args)
Amin Hassani75c5f942021-02-20 23:56:53 -0800741
Alex Klein1699fab2022-09-08 08:46:06 -0600742 self._current_root = current_root
743 self._ran_postinst = False
Amin Hassani75c5f942021-02-20 23:56:53 -0800744
Alex Klein1699fab2022-09-08 08:46:06 -0600745 def _GetPartitionName(self):
746 """See RawPartitionUpdater._GetPartitionName()."""
747 return constants.PART_ROOT_A
Amin Hassani75c5f942021-02-20 23:56:53 -0800748
Alex Klein1699fab2022-09-08 08:46:06 -0600749 def _GetRemotePartitionName(self):
750 """See RawPartitionUpdater._GetRemotePartitionName()."""
751 return constants.QUICK_PROVISION_PAYLOAD_ROOTFS
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800752
Alex Klein1699fab2022-09-08 08:46:06 -0600753 def _Run(self):
754 """The function that does the job of rootfs partition update."""
755 with ProgressWatcher(self._device, self._target):
756 super()._Run()
Amin Hassani75c5f942021-02-20 23:56:53 -0800757
Alex Klein1699fab2022-09-08 08:46:06 -0600758 self._RunPostInst()
Amin Hassani75c5f942021-02-20 23:56:53 -0800759
Alex Klein1699fab2022-09-08 08:46:06 -0600760 def _OptimizePartLocation(self, offset: int, length: int):
761 """Optimizes the size of the root partition of the image.
Amin Hassani75c5f942021-02-20 23:56:53 -0800762
Alex Klein975e86c2023-01-23 16:49:10 -0700763 Normally the file system does not occupy the entire partition.
764 Furthermore we don't need the verity hash tree at the end of the root
765 file system because postinst will recreate it. This function reads the
766 (approximate) superblock of the ext4 partition and extracts the actual
767 file system size in the root partition.
Alex Klein1699fab2022-09-08 08:46:06 -0600768 """
769 superblock_size = 4096 * 2
770 with open(self._image, "rb") as r:
771 r.seek(offset)
772 with tempfile.NamedTemporaryFile(delete=False) as fp:
773 fp.write(r.read(superblock_size))
774 fp.close()
775 return offset, partition_lib.Ext2FileSystemSize(fp.name)
Amin Hassani75c5f942021-02-20 23:56:53 -0800776
Alex Klein1699fab2022-09-08 08:46:06 -0600777 def _RunPostInst(self, on_target: bool = True):
778 """Runs the postinst process in the root partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800779
Alex Klein1699fab2022-09-08 08:46:06 -0600780 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600781 on_target: If true the postinst is run on the target (inactive)
782 partition. This is used when doing normal updates. If false, the
783 postinst is run on the current (active) partition. This is used
784 when reverting an update.
Alex Klein1699fab2022-09-08 08:46:06 -0600785 """
786 try:
787 postinst_dir = "/"
788 partition = self._current_root
789 if on_target:
790 postinst_dir = self._device.run(
791 ["mktemp", "-d", "-p", self._device.work_dir],
792 capture_output=True,
793 ).stdout.strip()
794 self._device.run(
795 ["mount", "-o", "ro", self._target, postinst_dir]
796 )
797 partition = self._target
Amin Hassani75c5f942021-02-20 23:56:53 -0800798
Alex Klein1699fab2022-09-08 08:46:06 -0600799 self._ran_postinst = True
800 postinst = os.path.join(postinst_dir, "postinst")
801 result = self._device.run(
802 [postinst, partition], capture_output=True
803 )
Amin Hassani75c5f942021-02-20 23:56:53 -0800804
Alex Klein1699fab2022-09-08 08:46:06 -0600805 logging.debug(
806 "Postinst result on %s: \n%s", postinst, result.stdout
807 )
808 # DeviceImagerOperation will look for this log.
809 logging.info("Postinstall completed.")
810 finally:
811 if on_target:
812 self._device.run(["umount", postinst_dir])
Amin Hassani75c5f942021-02-20 23:56:53 -0800813
Alex Klein1699fab2022-09-08 08:46:06 -0600814 def Revert(self):
815 """Reverts the root update install."""
816 logging.info("Reverting the rootfs partition update.")
817 if self._ran_postinst:
Alex Klein975e86c2023-01-23 16:49:10 -0700818 # We don't have to do anything for revert if we haven't changed the
819 # kernel priorities yet.
Alex Klein1699fab2022-09-08 08:46:06 -0600820 self._RunPostInst(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800821
822
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000823class MiniOSUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600824 """A class to update the miniOS partition on a Chromium OS device."""
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000825
Alex Klein1699fab2022-09-08 08:46:06 -0600826 def __init__(self, *args):
827 """Initializes the class.
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000828
Alex Klein1699fab2022-09-08 08:46:06 -0600829 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600830 *args: See PartitionUpdaterBase
Alex Klein1699fab2022-09-08 08:46:06 -0600831 """
832 super().__init__(*args)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000833
Alex Klein1699fab2022-09-08 08:46:06 -0600834 self._ran_postinst = False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000835
Alex Klein1699fab2022-09-08 08:46:06 -0600836 def _GetPartitionName(self):
837 """See RawPartitionUpdater._GetPartitionName()."""
838 return constants.PART_MINIOS_A
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000839
Alex Klein1699fab2022-09-08 08:46:06 -0600840 def _GetRemotePartitionName(self):
841 """See RawPartitionUpdater._GetRemotePartitionName()."""
842 return constants.QUICK_PROVISION_PAYLOAD_MINIOS
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000843
Alex Klein1699fab2022-09-08 08:46:06 -0600844 def _Run(self):
845 """The function that does the job of rootfs partition update."""
846 if self._image_type == ImageType.FULL:
847 if self._MiniOSPartitionsExistInImage():
848 logging.info("Updating miniOS partition from local.")
849 super()._Run()
850 else:
851 logging.warning(
852 "Not updating miniOS partition as it does not exist."
853 )
854 return
855 elif self._image_type == ImageType.REMOTE_DIRECTORY:
856 if not gs.GSContext().Exists(
857 os.path.join(
858 self._image, constants.QUICK_PROVISION_PAYLOAD_MINIOS
859 )
860 ):
861 logging.warning("Not updating miniOS, missing remote files.")
862 return
863 elif not self._MiniOSPartitionsExist():
864 logging.warning("Not updating miniOS, missing partitions.")
865 return
866 else:
867 logging.info("Updating miniOS partition from remote.")
868 super()._Run()
869 else:
870 # Let super() handle this error.
871 super()._Run()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000872
Alex Klein1699fab2022-09-08 08:46:06 -0600873 self._RunPostInstall()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000874
Alex Klein1699fab2022-09-08 08:46:06 -0600875 def _RunPostInstall(self):
876 """The function will change the priority of the miniOS partitions."""
877 self._FlipMiniOSPriority()
878 self._ran_postinst = True
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000879
Alex Klein1699fab2022-09-08 08:46:06 -0600880 def Revert(self):
881 """Reverts the miniOS partition update."""
882 if self._ran_postinst:
883 self._FlipMiniOSPriority()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000884
Alex Klein1699fab2022-09-08 08:46:06 -0600885 def _GetMiniOSPriority(self):
886 return self._device.run(
887 ["crossystem", constants.MINIOS_PRIORITY]
888 ).stdout
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000889
Alex Klein1699fab2022-09-08 08:46:06 -0600890 def _SetMiniOSPriority(self, priority: str):
891 self._device.run(
892 ["crossystem", f"{constants.MINIOS_PRIORITY}={priority}"]
893 )
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000894
Alex Klein1699fab2022-09-08 08:46:06 -0600895 def _FlipMiniOSPriority(self):
896 inactive_minios_priority = (
897 "B" if self._GetMiniOSPriority() == "A" else "A"
898 )
899 logging.info("Setting miniOS priority to %s", inactive_minios_priority)
900 self._SetMiniOSPriority(inactive_minios_priority)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000901
Alex Klein1699fab2022-09-08 08:46:06 -0600902 def _MiniOSPartitionsExistInImage(self):
903 """Checks if miniOS partition exists in the image."""
904 d = cgpt.Disk.FromImage(self._image)
905 try:
906 d.GetPartitionByTypeGuid(cgpt.MINIOS_TYPE_GUID)
907 return True
908 except KeyError:
909 return False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000910
Alex Klein1699fab2022-09-08 08:46:06 -0600911 def _MiniOSPartitionsExist(self):
912 """Checks if the device has miniOS partitions."""
913 run = lambda x: self._device.run(x).stdout.strip()
914 device_drive = run(["rootdev", "-s", "-d"])
915 cmd = ["cgpt", "show", "-t", device_drive, "-i"]
916 return all(
917 (run(cmd + [p]) == cgpt.MINIOS_TYPE_GUID) for p in ("9", "10")
918 )
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700919
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000920
Amin Hassani74403082021-02-22 11:40:09 -0800921class StatefulPayloadGenerator(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600922 """A class for generating a stateful update payload in a separate thread."""
Amin Hassani74403082021-02-22 11:40:09 -0800923
Alex Klein1699fab2022-09-08 08:46:06 -0600924 def __init__(self, image: str):
925 """Initializes that class.
Amin Hassani74403082021-02-22 11:40:09 -0800926
Alex Klein1699fab2022-09-08 08:46:06 -0600927 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600928 image: The path to a local Chromium OS image.
Alex Klein1699fab2022-09-08 08:46:06 -0600929 """
930 super().__init__()
931 self._image = image
932
933 def run(self):
934 """Generates the stateful update and writes it into the output pipe."""
935 try:
936 paygen_stateful_payload_lib.GenerateStatefulPayload(
937 self._image, self._Source()
938 )
939 finally:
940 self._CloseSource()
Amin Hassani74403082021-02-22 11:40:09 -0800941
942
943class StatefulUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600944 """A class to update the stateful partition on a device."""
Amin Hassani74403082021-02-22 11:40:09 -0800945
Alex Klein1699fab2022-09-08 08:46:06 -0600946 def __init__(self, clobber_stateful: bool, *args):
947 """Initializes the class
Amin Hassani74403082021-02-22 11:40:09 -0800948
Alex Klein1699fab2022-09-08 08:46:06 -0600949 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600950 clobber_stateful: Whether to clobber the stateful or not.
951 *args: Look at PartitionUpdaterBase.
Alex Klein1699fab2022-09-08 08:46:06 -0600952 """
953 super().__init__(*args)
954 self._clobber_stateful = clobber_stateful
Amin Hassani74403082021-02-22 11:40:09 -0800955
Alex Klein1699fab2022-09-08 08:46:06 -0600956 def _Run(self):
Alex Klein975e86c2023-01-23 16:49:10 -0700957 """Read/Download the stateful updates and write it into the device."""
Alex Klein1699fab2022-09-08 08:46:06 -0600958 if self._image_type == ImageType.FULL:
959 generator_cls = StatefulPayloadGenerator
960 elif self._image_type == ImageType.REMOTE_DIRECTORY:
961 generator_cls = GsFileCopier
962 self._image = os.path.join(
963 self._image, paygen_stateful_payload_lib.STATEFUL_FILE
964 )
965 else:
966 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassani74403082021-02-22 11:40:09 -0800967
Alex Klein1699fab2022-09-08 08:46:06 -0600968 with generator_cls(self._image) as generator:
969 try:
970 updater = stateful_updater.StatefulUpdater(self._device)
971 updater.Update(
972 generator.Target(),
973 is_payload_on_device=False,
974 update_type=(
975 stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER
976 if self._clobber_stateful
977 else None
978 ),
979 )
980 finally:
981 generator.CloseTarget()
982
983 def Revert(self):
984 """Reverts the stateful partition update."""
985 logging.info("Reverting the stateful update.")
986 stateful_updater.StatefulUpdater(self._device).Reset()
Amin Hassani55970562021-02-22 20:49:13 -0800987
988
989class ProgressWatcher(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600990 """A class used for watching the progress of rootfs update."""
Amin Hassani55970562021-02-22 20:49:13 -0800991
Alex Klein1699fab2022-09-08 08:46:06 -0600992 def __init__(self, device, target_root: str):
993 """Initializes the class.
Amin Hassani55970562021-02-22 20:49:13 -0800994
Alex Klein1699fab2022-09-08 08:46:06 -0600995 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600996 device: The ChromiumOSDevice to be updated.
997 target_root: The target root partition to monitor the progress of.
Alex Klein1699fab2022-09-08 08:46:06 -0600998 """
999 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -08001000
Alex Klein1699fab2022-09-08 08:46:06 -06001001 self._device = device
1002 self._target_root = target_root
1003 self._exit = False
Amin Hassani55970562021-02-22 20:49:13 -08001004
Alex Klein1699fab2022-09-08 08:46:06 -06001005 def __enter__(self):
1006 """Starts the thread."""
1007 self.start()
1008 return self
Amin Hassani55970562021-02-22 20:49:13 -08001009
Alex Klein1699fab2022-09-08 08:46:06 -06001010 def __exit__(self, *args, **kwargs):
1011 """Exists the thread."""
1012 self._exit = True
1013 self.join()
Amin Hassani55970562021-02-22 20:49:13 -08001014
Alex Klein1699fab2022-09-08 08:46:06 -06001015 def _ShouldExit(self):
1016 return self._exit
Amin Hassani55970562021-02-22 20:49:13 -08001017
Alex Klein1699fab2022-09-08 08:46:06 -06001018 def run(self):
1019 """Monitors the progress of the target root partitions' update.
Amin Hassani55970562021-02-22 20:49:13 -08001020
Alex Klein975e86c2023-01-23 16:49:10 -07001021 This is done by periodically, reading the fd position of the process
1022 that is writing into the target partition and reporting it back. Then
1023 the position is divided by the size of the block device to report
1024 approximate progress.
Alex Klein1699fab2022-09-08 08:46:06 -06001025 """
1026 cmd = ["blockdev", "--getsize64", self._target_root]
Amin Hassani55970562021-02-22 20:49:13 -08001027 output = self._device.run(cmd, capture_output=True).stdout.strip()
Alex Klein1699fab2022-09-08 08:46:06 -06001028 if output is None:
1029 raise Error(f"Cannot get the block device size from {output}.")
1030 dev_size = int(output)
1031
1032 # Using lsof to find out which process is writing to the target rootfs.
1033 cmd = ["lsof", "-t", self._target_root]
Brian Norris7ceb0fe2022-11-10 17:46:31 -08001034 while True:
1035 if self._ShouldExit():
1036 return
1037
Alex Klein1699fab2022-09-08 08:46:06 -06001038 try:
1039 pid = self._device.run(cmd, capture_output=True).stdout.strip()
1040 if pid:
1041 break
1042 except cros_build_lib.RunCommandError:
1043 continue
1044 finally:
1045 time.sleep(1)
1046
Alex Klein975e86c2023-01-23 16:49:10 -07001047 # Now that we know which process is writing to it, we can look the
1048 # fdinfo of stdout of that process to get its offset. We're assuming
1049 # there will be no seek, which is correct.
Alex Klein1699fab2022-09-08 08:46:06 -06001050 cmd = ["cat", f"/proc/{pid}/fdinfo/1"]
1051 while not self._ShouldExit():
1052 try:
1053 output = self._device.run(
1054 cmd, capture_output=True
1055 ).stdout.strip()
1056 m = re.search(r"^pos:\s*(\d+)$", output, flags=re.M)
1057 if m:
1058 offset = int(m.group(1))
1059 # DeviceImagerOperation will look for this log.
1060 logging.info("RootFS progress: %f", offset / dev_size)
1061 except cros_build_lib.RunCommandError:
1062 continue
1063 finally:
1064 time.sleep(1)
Amin Hassani55970562021-02-22 20:49:13 -08001065
1066
1067class DeviceImagerOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -06001068 """A class to provide a progress bar for DeviceImager operation."""
Amin Hassani55970562021-02-22 20:49:13 -08001069
Alex Klein1699fab2022-09-08 08:46:06 -06001070 def __init__(self):
1071 """Initializes the class."""
1072 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -08001073
Alex Klein1699fab2022-09-08 08:46:06 -06001074 self._progress = 0.0
Amin Hassani55970562021-02-22 20:49:13 -08001075
Alex Klein1699fab2022-09-08 08:46:06 -06001076 def ParseOutput(self, output=None):
1077 """Override function to parse the output and provide progress.
Amin Hassani55970562021-02-22 20:49:13 -08001078
Alex Klein1699fab2022-09-08 08:46:06 -06001079 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001080 output: The stderr or stdout.
Alex Klein1699fab2022-09-08 08:46:06 -06001081 """
1082 output = self._stdout.read()
1083 match = re.findall(r"RootFS progress: (\d+(?:\.\d+)?)", output)
1084 if match:
1085 progress = float(match[0])
1086 self._progress = max(self._progress, progress)
Amin Hassani55970562021-02-22 20:49:13 -08001087
Alex Klein1699fab2022-09-08 08:46:06 -06001088 # If postinstall completes, move half of the remaining progress.
1089 if re.findall(r"Postinstall completed", output):
1090 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001091
Alex Klein975e86c2023-01-23 16:49:10 -07001092 # While waiting for reboot, each time, move half of the remaining
1093 # progress.
Alex Klein1699fab2022-09-08 08:46:06 -06001094 if re.findall(r"Unable to get new boot_id", output):
1095 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001096
Alex Klein1699fab2022-09-08 08:46:06 -06001097 if re.findall(r"DeviceImager completed.", output):
1098 self._progress = 1.0
Amin Hassani55970562021-02-22 20:49:13 -08001099
Alex Klein1699fab2022-09-08 08:46:06 -06001100 self.ProgressBar(self._progress)