blob: b9b96db1c0a2d9e3261f7260116378e2b21f06bc [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2021 The ChromiumOS Authors
Amin Hassani92f6c4a2021-02-20 17:36:09 -08002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Library containing functions to install an image on a Chromium OS device."""
6
Amin Hassanid4b3ff82021-02-20 23:05:14 -08007import abc
Amin Hassani92f6c4a2021-02-20 17:36:09 -08008import enum
Daichi Hironoc1a8fd32022-01-07 22:17:51 +09009from io import BytesIO
Chris McDonald14ac61d2021-07-21 11:49:56 -060010import logging
Amin Hassani92f6c4a2021-02-20 17:36:09 -080011import os
12import re
Amin Hassanid4b3ff82021-02-20 23:05:14 -080013import tempfile
14import threading
Amin Hassani55970562021-02-22 20:49:13 -080015import time
Daichi Hironoc1a8fd32022-01-07 22:17:51 +090016from typing import Dict, List, Tuple, Union
Amin Hassani92f6c4a2021-02-20 17:36:09 -080017
Amin Hassani55970562021-02-22 20:49:13 -080018from chromite.cli import command
Amin Hassanicf8f0042021-03-12 10:42:13 -080019from chromite.cli import flash
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000020from chromite.lib import cgpt
Amin Hassanid4b3ff82021-02-20 23:05:14 -080021from chromite.lib import constants
Amin Hassani92f6c4a2021-02-20 17:36:09 -080022from chromite.lib import cros_build_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080023from chromite.lib import gs
Amin Hassanid4b3ff82021-02-20 23:05:14 -080024from chromite.lib import image_lib
Amin Hassani55970562021-02-22 20:49:13 -080025from chromite.lib import operation
Amin Hassanid4b3ff82021-02-20 23:05:14 -080026from chromite.lib import osutils
Amin Hassani92f6c4a2021-02-20 17:36:09 -080027from chromite.lib import parallel
28from chromite.lib import remote_access
29from chromite.lib import retry_util
Amin Hassani74403082021-02-22 11:40:09 -080030from chromite.lib import stateful_updater
Amin Hassani75c5f942021-02-20 23:56:53 -080031from chromite.lib.paygen import partition_lib
Amin Hassani74403082021-02-22 11:40:09 -080032from chromite.lib.paygen import paygen_stateful_payload_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080033from chromite.lib.xbuddy import devserver_constants
34from chromite.lib.xbuddy import xbuddy
Alex Klein18ef1212021-10-14 12:49:02 -060035from chromite.utils import timer
Amin Hassani92f6c4a2021-02-20 17:36:09 -080036
37
Amin Hassani92f6c4a2021-02-20 17:36:09 -080038class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060039 """Thrown when there is a general Chromium OS-specific flash error."""
Amin Hassani92f6c4a2021-02-20 17:36:09 -080040
41
42class ImageType(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060043 """Type of the image that is used for flashing the device."""
Amin Hassani92f6c4a2021-02-20 17:36:09 -080044
Alex Klein1699fab2022-09-08 08:46:06 -060045 # The full image on disk (e.g. chromiumos_test_image.bin).
46 FULL = 0
47 # The remote directory path
48 # (e.g gs://chromeos-image-archive/eve-release/R90-x.x.x)
49 REMOTE_DIRECTORY = 1
Amin Hassani92f6c4a2021-02-20 17:36:09 -080050
51
52class Partition(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -060053 """An enum for partition types like kernel and rootfs."""
54
55 KERNEL = 0
56 ROOTFS = 1
57 MINIOS = 2
Amin Hassani92f6c4a2021-02-20 17:36:09 -080058
59
60class DeviceImager(object):
Alex Klein1699fab2022-09-08 08:46:06 -060061 """A class to flash a Chromium OS device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080062
Alex Klein1699fab2022-09-08 08:46:06 -060063 This utility uses parallelism as much as possible to achieve its goal as fast
64 as possible. For example, it uses parallel compressors, parallel transfers,
65 and simultaneous pipes.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080066 """
67
Alex Klein1699fab2022-09-08 08:46:06 -060068 # The parameters of the kernel and rootfs's two main partitions.
69 A = {Partition.KERNEL: 2, Partition.ROOTFS: 3}
70 B = {Partition.KERNEL: 4, Partition.ROOTFS: 5}
Amin Hassani92f6c4a2021-02-20 17:36:09 -080071
Alex Klein1699fab2022-09-08 08:46:06 -060072 MINIOS_A = {Partition.MINIOS: 9}
73 MINIOS_B = {Partition.MINIOS: 10}
Amin Hassani92f6c4a2021-02-20 17:36:09 -080074
Alex Klein1699fab2022-09-08 08:46:06 -060075 def __init__(
76 self,
77 device,
78 image: str,
79 board: str = None,
80 version: str = None,
81 no_rootfs_update: bool = False,
82 no_stateful_update: bool = False,
83 no_minios_update: bool = False,
84 no_reboot: bool = False,
85 disable_verification: bool = False,
86 clobber_stateful: bool = False,
87 clear_tpm_owner: bool = False,
88 delta: bool = False,
89 ):
90 """Initialize DeviceImager for flashing a Chromium OS device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080091
Alex Klein1699fab2022-09-08 08:46:06 -060092 Args:
93 device: The ChromiumOSDevice to be updated.
94 image: The target image path (can be xBuddy path).
95 board: Board to use.
96 version: Image version to use.
97 no_rootfs_update: Whether to do rootfs partition update.
98 no_stateful_update: Whether to do stateful partition update.
99 no_minios_update: Whether to do minios partition update.
100 no_reboot: Whether to reboot device after update. The default is True.
101 disable_verification: Whether to disabling rootfs verification on the
102 device.
103 clobber_stateful: Whether to do a clean stateful partition.
104 clear_tpm_owner: If true, it will clear the TPM owner on reboot.
105 delta: Whether to use delta compression when transferring image bytes.
106 """
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800107
Alex Klein1699fab2022-09-08 08:46:06 -0600108 self._device = device
109 self._image = image
110 self._board = board
111 self._version = version
112 self._no_rootfs_update = no_rootfs_update
113 self._no_stateful_update = no_stateful_update
114 self._no_minios_update = no_minios_update
115 self._no_reboot = no_reboot
116 self._disable_verification = disable_verification
117 self._clobber_stateful = clobber_stateful
118 self._clear_tpm_owner = clear_tpm_owner
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800119
Alex Klein1699fab2022-09-08 08:46:06 -0600120 self._image_type = None
121 self._inactive_state = None
122 self._delta = delta
Daichi Hirono28831b3b2022-04-07 12:41:11 +0900123
Alex Klein1699fab2022-09-08 08:46:06 -0600124 def Run(self):
125 """Update the device with image of specific version."""
126 self._LocateImage()
127 logging.notice(
128 "Preparing to update the remote device %s with image %s",
129 self._device.hostname,
130 self._image,
131 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800132
Alex Klein1699fab2022-09-08 08:46:06 -0600133 try:
134 if command.UseProgressBar():
135 op = DeviceImagerOperation()
136 op.Run(self._Run)
137 else:
138 self._Run()
139 except Exception as e:
140 raise Error(f"DeviceImager Failed with error: {e}")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800141
Alex Klein1699fab2022-09-08 08:46:06 -0600142 # DeviceImagerOperation will look for this log.
143 logging.info("DeviceImager completed.")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800144
Alex Klein1699fab2022-09-08 08:46:06 -0600145 def _Run(self):
146 """Runs the various operations to install the image on device."""
147 # TODO(b/228389041): Switch to delta compression if self._delta is True
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800148
Alex Klein1699fab2022-09-08 08:46:06 -0600149 self._InstallPartitions()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800150
Alex Klein1699fab2022-09-08 08:46:06 -0600151 if self._clear_tpm_owner:
152 self._device.ClearTpmOwner()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800153
Alex Klein1699fab2022-09-08 08:46:06 -0600154 if not self._no_reboot:
155 self._Reboot()
156 self._VerifyBootExpectations()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800157
Alex Klein1699fab2022-09-08 08:46:06 -0600158 if self._disable_verification:
159 self._device.DisableRootfsVerification()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800160
Alex Klein1699fab2022-09-08 08:46:06 -0600161 def _LocateImage(self):
162 """Locates the path to the final image(s) that need to be installed.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800163
Alex Klein1699fab2022-09-08 08:46:06 -0600164 If the paths is local, the image should be the Chromium OS GPT image
165 (e.g. chromiumos_test_image.bin). If the path is remote, it should be the
166 remote directory where we can find the quick-provision and stateful update
167 files (e.g. gs://chromeos-image-archive/eve-release/R90-x.x.x).
Amin Hassanicf8f0042021-03-12 10:42:13 -0800168
Alex Klein1699fab2022-09-08 08:46:06 -0600169 NOTE: At this point there is no caching involved. Hence we always download
170 the partition payloads or extract them from the Chromium OS image.
171 """
172 if os.path.isfile(self._image):
173 self._image_type = ImageType.FULL
174 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800175
Alex Klein1699fab2022-09-08 08:46:06 -0600176 # TODO(b/172212406): We could potentially also allow this by searching
177 # through the directory to see whether we have quick-provision and stateful
178 # payloads. This only makes sense when a user has their workstation at home
179 # and doesn't want to incur the bandwidth cost of downloading the same
180 # image multiple times. For that, they can simply download the GPT image
181 # image first and flash that instead.
182 if os.path.isdir(self._image):
183 raise ValueError(
184 f"{self._image}: input must be a disk image, not a directory."
185 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800186
Alex Klein1699fab2022-09-08 08:46:06 -0600187 if gs.PathIsGs(self._image):
188 # TODO(b/172212406): Check whether it is a directory. If it wasn't a
189 # directory download the image into some temp location and use it instead.
190 self._image_type = ImageType.REMOTE_DIRECTORY
191 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800192
Alex Klein1699fab2022-09-08 08:46:06 -0600193 # Assuming it is an xBuddy path.
194 board = cros_build_lib.GetBoard(
195 device_board=self._device.board or flash.GetDefaultBoard(),
196 override_board=self._board,
197 force=True,
198 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800199
Alex Klein1699fab2022-09-08 08:46:06 -0600200 xb = xbuddy.XBuddy(board=board, version=self._version)
201 build_id, local_file = xb.Translate([self._image])
202 if build_id is None:
203 raise Error(f"{self._image}: unable to find matching xBuddy path.")
204 logging.info("XBuddy path translated to build ID %s", build_id)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800205
Alex Klein1699fab2022-09-08 08:46:06 -0600206 if local_file:
207 self._image = local_file
208 self._image_type = ImageType.FULL
209 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800210
Alex Klein1699fab2022-09-08 08:46:06 -0600211 self._image = f"{devserver_constants.GS_IMAGE_DIR}/{build_id}"
212 self._image_type = ImageType.REMOTE_DIRECTORY
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800213
Alex Klein1699fab2022-09-08 08:46:06 -0600214 def _SplitDevPath(self, path: str) -> Tuple[str, int]:
215 """Splits the given /dev/x path into prefix and the dev number.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800216
Alex Klein1699fab2022-09-08 08:46:06 -0600217 Args:
218 path: The path to a block dev device.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800219
Alex Klein1699fab2022-09-08 08:46:06 -0600220 Returns:
221 A tuple of representing the prefix and the index of the dev path.
222 e.g.: '/dev/mmcblk0p1' -> ['/dev/mmcblk0p', 1]
223 """
224 match = re.search(r"(.*)([0-9]+)$", path)
225 if match is None:
226 raise Error(f"{path}: Could not parse root dev path.")
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000227
Alex Klein1699fab2022-09-08 08:46:06 -0600228 return match.group(1), int(match.group(2))
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000229
Alex Klein1699fab2022-09-08 08:46:06 -0600230 def _GetKernelState(self, root_num: int) -> Tuple[Dict, Dict]:
231 """Returns the kernel state.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800232
Alex Klein1699fab2022-09-08 08:46:06 -0600233 Returns:
234 A tuple of two dictionaries: The current active kernel state and the
235 inactive kernel state. (Look at A and B constants in this class.)
236 """
237 if root_num == self.A[Partition.ROOTFS]:
238 return self.A, self.B
239 elif root_num == self.B[Partition.ROOTFS]:
240 return self.B, self.A
241 else:
242 raise Error(f"Invalid root partition number {root_num}")
Amin Hassanid684e982021-02-26 11:10:58 -0800243
Alex Klein1699fab2022-09-08 08:46:06 -0600244 def _GetMiniOSState(self, minios_num: int) -> Tuple[Dict, Dict]:
245 """Returns the miniOS state.
Amin Hassani75c5f942021-02-20 23:56:53 -0800246
Alex Klein1699fab2022-09-08 08:46:06 -0600247 Returns:
248 A tuple of dictionaries: The current active miniOS state and the inactive
249 miniOS state.
250 """
251 if minios_num == self.MINIOS_A[Partition.MINIOS]:
252 return self.MINIOS_A, self.MINIOS_B
253 elif minios_num == self.MINIOS_B[Partition.MINIOS]:
254 return self.MINIOS_B, self.MINIOS_A
255 else:
256 raise Error(f"Invalid minios partition number {minios_num}")
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800257
Alex Klein1699fab2022-09-08 08:46:06 -0600258 def _InstallPartitions(self):
259 """The main method that installs the partitions of a Chrome OS device.
Amin Hassani74403082021-02-22 11:40:09 -0800260
Alex Klein1699fab2022-09-08 08:46:06 -0600261 It uses parallelism to install the partitions as fast as possible.
262 """
263 prefix, root_num = self._SplitDevPath(self._device.root_dev)
264 active_state, self._inactive_state = self._GetKernelState(root_num)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000265
Alex Klein1699fab2022-09-08 08:46:06 -0600266 updaters = []
267 if not self._no_rootfs_update:
268 current_root = prefix + str(active_state[Partition.ROOTFS])
269 target_root = prefix + str(self._inactive_state[Partition.ROOTFS])
270 updaters.append(
271 RootfsUpdater(
272 current_root,
273 self._device,
274 self._image,
275 self._image_type,
276 target_root,
277 )
278 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800279
Alex Klein1699fab2022-09-08 08:46:06 -0600280 target_kernel = prefix + str(self._inactive_state[Partition.KERNEL])
281 updaters.append(
282 KernelUpdater(
283 self._device, self._image, self._image_type, target_kernel
284 )
285 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800286
Alex Klein1699fab2022-09-08 08:46:06 -0600287 if not self._no_stateful_update:
288 updaters.append(
289 StatefulUpdater(
290 self._clobber_stateful,
291 self._device,
292 self._image,
293 self._image_type,
294 None,
295 )
296 )
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800297
Alex Klein1699fab2022-09-08 08:46:06 -0600298 if not self._no_minios_update:
299 minios_priority = self._device.run(
300 ["crossystem", constants.MINIOS_PRIORITY]
301 ).stdout
302 if minios_priority not in ["A", "B"]:
303 logging.warning(
304 "Skipping miniOS flash due to missing priority."
305 )
306 else:
307 # Reference disk_layout_v3 for partition numbering.
308 _, inactive_minios_state = self._GetMiniOSState(
309 9 if minios_priority == "A" else 10
310 )
311 target_minios = prefix + str(
312 inactive_minios_state[Partition.MINIOS]
313 )
314 minios_updater = MiniOSUpdater(
315 self._device, self._image, self._image_type, target_minios
316 )
317 updaters.append(minios_updater)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800318
Alex Klein1699fab2022-09-08 08:46:06 -0600319 # Retry the partitions updates that failed, in case a transient error (like
320 # SSH drop, etc) caused the error.
321 num_retries = 1
322 try:
323 retry_util.RetryException(
324 Error,
325 num_retries,
326 parallel.RunParallelSteps,
327 (x.Run for x in updaters if not x.IsFinished()),
328 halt_on_error=True,
329 )
330 except Exception:
331 # If one of the partitions failed to be installed, revert all partitions.
332 parallel.RunParallelSteps(x.Revert for x in updaters)
333 raise
334
335 def _Reboot(self):
336 """Reboots the device."""
337 try:
338 self._device.Reboot(timeout_sec=300)
339 except remote_access.RebootError:
340 raise Error(
341 "Could not recover from reboot. Once example reason"
342 " could be the image provided was a non-test image"
343 " or the system failed to boot after the update."
344 )
345 except Exception as e:
346 raise Error(f"Failed to reboot to the device with error: {e}")
347
348 def _VerifyBootExpectations(self):
349 """Verify that we fully booted into the expected kernel state."""
350 # Discover the newly active kernel.
351 _, root_num = self._SplitDevPath(self._device.root_dev)
352 active_state, _ = self._GetKernelState(root_num)
353
354 # If this happens, we should rollback.
355 if active_state != self._inactive_state:
356 raise Error("The expected kernel state after update is invalid.")
357
358 logging.info("Verified boot expectations.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800359
360
361class ReaderBase(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600362 """The base class for reading different inputs and writing into output.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800363
Alex Klein1699fab2022-09-08 08:46:06 -0600364 This class extends threading.Thread, so it will be run on its own thread. Also
365 it can be used as a context manager. Internally, it opens necessary files for
366 writing to and reading from. This class cannot be instantiated, it needs to be
367 sub-classed first to provide necessary function implementations.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800368 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800369
Alex Klein1699fab2022-09-08 08:46:06 -0600370 def __init__(self, use_named_pipes: bool = False):
371 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800372
Alex Klein1699fab2022-09-08 08:46:06 -0600373 Args:
374 use_named_pipes: Whether to use a named pipe or anonymous file
375 descriptors.
376 """
377 super().__init__()
378 self._use_named_pipes = use_named_pipes
379 self._pipe_target = None
380 self._pipe_source = None
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800381
Alex Klein1699fab2022-09-08 08:46:06 -0600382 def __del__(self):
383 """Destructor.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800384
Alex Klein1699fab2022-09-08 08:46:06 -0600385 Make sure to clean up any named pipes we might have created.
386 """
387 if self._use_named_pipes:
388 osutils.SafeUnlink(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800389
Alex Klein1699fab2022-09-08 08:46:06 -0600390 def __enter__(self):
391 """Enters the context manager"""
392 if self._use_named_pipes:
393 # There is no need for the temp file, we only need its path. So the named
394 # pipe is created after this temp file is deleted.
395 with tempfile.NamedTemporaryFile(
396 prefix="chromite-device-imager"
397 ) as fp:
398 self._pipe_target = self._pipe_source = fp.name
399 os.mkfifo(self._pipe_target)
400 else:
401 self._pipe_target, self._pipe_source = os.pipe()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800402
Alex Klein1699fab2022-09-08 08:46:06 -0600403 self.start()
404 return self
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800405
Alex Klein1699fab2022-09-08 08:46:06 -0600406 def __exit__(self, *args, **kwargs):
407 """Exits the context manager."""
408 self.join()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800409
Alex Klein1699fab2022-09-08 08:46:06 -0600410 def _Source(self):
411 """Returns the source pipe to write data into.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800412
Alex Klein1699fab2022-09-08 08:46:06 -0600413 Sub-classes can use this function to determine where to write their data
414 into.
415 """
416 return self._pipe_source
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800417
Alex Klein1699fab2022-09-08 08:46:06 -0600418 def _CloseSource(self):
419 """Closes the source pipe.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800420
Alex Klein1699fab2022-09-08 08:46:06 -0600421 Sub-classes should use this function to close the pipe after they are done
422 writing into it. Failure to do so may result reader of the data to hang
423 indefinitely.
424 """
425 if not self._use_named_pipes:
426 os.close(self._pipe_source)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800427
Alex Klein1699fab2022-09-08 08:46:06 -0600428 def Target(self):
429 """Returns the target pipe to read data from.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800430
Alex Klein1699fab2022-09-08 08:46:06 -0600431 Users of this class can use this path to read data from.
432 """
433 return self._pipe_target
434
435 def CloseTarget(self):
436 """Closes the target pipe.
437
438 Users of this class should use this function to close the pipe after they
439 are done reading from it.
440 """
441 if self._use_named_pipes:
442 os.remove(self._pipe_target)
443 else:
444 os.close(self._pipe_target)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800445
446
447class PartialFileReader(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600448 """A class to read specific offset and length from a file and compress it.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800449
Alex Klein1699fab2022-09-08 08:46:06 -0600450 This class can be used to read from specific location and length in a file
451 (e.g. A partition in a GPT image). Then it compresses the input and writes it
452 out (to a pipe). Look at the base class for more information.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800453 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800454
Alex Klein1699fab2022-09-08 08:46:06 -0600455 # The offset of different partitions in a Chromium OS image does not always
456 # align to larger values like 4096. It seems that 512 is the maximum value to
457 # be divisible by partition offsets. This size should not be increased just
458 # for 'performance reasons'. Since we are doing everything in parallel, in
459 # practice there is not much difference between this and larger block sizes as
460 # parallelism hides the possible extra latency provided by smaller block
461 # sizes.
462 _BLOCK_SIZE = 512
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800463
Alex Klein1699fab2022-09-08 08:46:06 -0600464 def __init__(
465 self,
466 image: str,
467 offset: int,
468 length: int,
469 compression_command: List[str],
470 ):
471 """Initializes the class.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800472
Alex Klein1699fab2022-09-08 08:46:06 -0600473 Args:
474 image: The path to an image (local or remote directory).
475 offset: The offset (in bytes) to read from the image.
476 length: The length (in bytes) to read from the image.
477 compression_command: The command to compress transferred bytes.
478 """
479 super().__init__()
480
481 self._image = image
482 self._offset = offset
483 self._length = length
484 self._compression_command = compression_command
485
486 def run(self):
487 """Runs the reading and compression."""
488 cmd = [
489 "dd",
490 "status=none",
491 f"if={self._image}",
492 f"ibs={self._BLOCK_SIZE}",
493 f"skip={int(self._offset/self._BLOCK_SIZE)}",
494 f"count={int(self._length/self._BLOCK_SIZE)}",
495 "|",
496 *self._compression_command,
497 ]
498
499 try:
500 cros_build_lib.run(" ".join(cmd), stdout=self._Source(), shell=True)
501 finally:
502 self._CloseSource()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800503
504
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800505class GsFileCopier(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600506 """A class for downloading gzip compressed file from GS bucket into a pipe."""
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800507
Alex Klein1699fab2022-09-08 08:46:06 -0600508 def __init__(self, image: str):
509 """Initializes the class.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800510
Alex Klein1699fab2022-09-08 08:46:06 -0600511 Args:
512 image: The path to an image (local or remote directory).
513 """
514 super().__init__(use_named_pipes=True)
515 self._image = image
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800516
Alex Klein1699fab2022-09-08 08:46:06 -0600517 def run(self):
518 """Runs the download and write into the output pipe."""
519 try:
520 gs.GSContext().Copy(self._image, self._Source())
521 finally:
522 self._CloseSource()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800523
524
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800525class PartitionUpdaterBase(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600526 """A base abstract class to use for installing an image into a partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800527
Alex Klein1699fab2022-09-08 08:46:06 -0600528 Sub-classes should implement the abstract methods to provide the core
529 functionality.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800530 """
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800531
Alex Klein1699fab2022-09-08 08:46:06 -0600532 def __init__(self, device, image: str, image_type, target: str):
533 """Initializes this base class with values that most sub-classes will need.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800534
Alex Klein1699fab2022-09-08 08:46:06 -0600535 Args:
536 device: The ChromiumOSDevice to be updated.
537 image: The target image path for the partition update.
538 image_type: The type of the image (ImageType).
539 target: The target path (e.g. block dev) to install the update.
540 """
541 self._device = device
542 self._image = image
543 self._image_type = image_type
544 self._target = target
545 self._finished = False
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800546
Alex Klein1699fab2022-09-08 08:46:06 -0600547 def Run(self):
548 """The main function that does the partition update job."""
549 with timer.Timer() as t:
550 try:
551 self._Run()
552 finally:
553 self._finished = True
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800554
Alex Klein1699fab2022-09-08 08:46:06 -0600555 logging.debug("Completed %s in %s", self.__class__.__name__, t)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800556
Alex Klein1699fab2022-09-08 08:46:06 -0600557 @abc.abstractmethod
558 def _Run(self):
559 """The method that need to be implemented by sub-classes."""
560 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800561
Alex Klein1699fab2022-09-08 08:46:06 -0600562 def IsFinished(self):
563 """Returns whether the partition update has been successful."""
564 return self._finished
565
566 @abc.abstractmethod
567 def Revert(self):
568 """Reverts the partition update.
569
570 Sub-classes need to implement this function to provide revert capability.
571 """
572 raise NotImplementedError("Sub-classes need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800573
574
575class RawPartitionUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600576 """A class to update a raw partition on a Chromium OS device."""
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800577
Alex Klein1699fab2022-09-08 08:46:06 -0600578 def _Run(self):
579 """The function that does the job of kernel partition update."""
580 if self._image_type == ImageType.FULL:
581 self._CopyPartitionFromImage(self._GetPartitionName())
582 elif self._image_type == ImageType.REMOTE_DIRECTORY:
583 self._RedirectPartition(self._GetRemotePartitionName())
584 else:
585 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800586
Alex Klein1699fab2022-09-08 08:46:06 -0600587 def _GetPartitionName(self):
588 """Returns the name of the partition in a Chromium OS GPT layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800589
Alex Klein1699fab2022-09-08 08:46:06 -0600590 Subclasses should override this function to return correct name.
591 """
592 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800593
Alex Klein1699fab2022-09-08 08:46:06 -0600594 def _CopyPartitionFromImage(self, part_name: str):
595 """Updates the device's partition from a local Chromium OS image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800596
Alex Klein1699fab2022-09-08 08:46:06 -0600597 Args:
598 part_name: The name of the partition in the source image that needs to be
599 extracted.
600 """
601 offset, length = self._GetPartLocation(part_name)
602 offset, length = self._OptimizePartLocation(offset, length)
603 compressor, decompressor = self._GetCompressionAndDecompression()
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900604
Alex Klein1699fab2022-09-08 08:46:06 -0600605 with PartialFileReader(
606 self._image, offset, length, compressor
607 ) as generator:
608 try:
609 self._WriteToTarget(generator.Target(), decompressor)
610 finally:
611 generator.CloseTarget()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800612
Alex Klein1699fab2022-09-08 08:46:06 -0600613 def _GetCompressionAndDecompression(self) -> Tuple[List[str], List[str]]:
614 """Returns compression / decompression commands."""
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900615
Alex Klein1699fab2022-09-08 08:46:06 -0600616 return (
Mike Frysinger66306012022-04-22 15:23:13 -0400617 [
618 cros_build_lib.FindCompressor(
619 cros_build_lib.CompressionType.GZIP
620 )
621 ],
622 self._device.GetDecompressor(cros_build_lib.CompressionType.GZIP),
Alex Klein1699fab2022-09-08 08:46:06 -0600623 )
Daichi Hironoc1a8fd32022-01-07 22:17:51 +0900624
Alex Klein1699fab2022-09-08 08:46:06 -0600625 def _WriteToTarget(
626 self, source: Union[int, BytesIO], decompress_command: List[str]
627 ) -> None:
628 """Writes bytes source to the target device on DUT.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800629
Alex Klein1699fab2022-09-08 08:46:06 -0600630 Returns:
631 A string command to run on a device to read data from stdin, uncompress it
632 and write it to the target partition.
633 """
634 # Using oflag=direct to tell the OS not to cache the writes (faster).
635 cmd = " ".join(
636 [
637 *decompress_command,
638 "|",
639 "dd",
640 "bs=1M",
641 "oflag=direct",
642 f"of={self._target}",
643 ]
644 )
645 self._device.run(cmd, input=source, shell=True)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800646
Alex Klein1699fab2022-09-08 08:46:06 -0600647 def _GetPartLocation(self, part_name: str):
648 """Extracts the location and size of the raw partition from the image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800649
Alex Klein1699fab2022-09-08 08:46:06 -0600650 Args:
651 part_name: The name of the partition in the source image that needs to be
652 extracted.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800653
Alex Klein1699fab2022-09-08 08:46:06 -0600654 Returns:
655 A tuple of offset and length (in bytes) from the image.
656 """
657 try:
658 parts = image_lib.GetImageDiskPartitionInfo(self._image)
659 part_info = [p for p in parts if p.name == part_name][0]
660 except IndexError:
661 raise Error(f"No partition named {part_name} found.")
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800662
Alex Klein1699fab2022-09-08 08:46:06 -0600663 return int(part_info.start), int(part_info.size)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800664
Alex Klein1699fab2022-09-08 08:46:06 -0600665 def _GetRemotePartitionName(self):
666 """Returns the name of the quick-provision partition file.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800667
Alex Klein1699fab2022-09-08 08:46:06 -0600668 Subclasses should override this function to return correct name.
669 """
670 raise NotImplementedError("Subclasses need to implement this.")
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800671
Alex Klein1699fab2022-09-08 08:46:06 -0600672 def _OptimizePartLocation(self, offset: int, length: int):
673 """Optimizes the offset and length of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800674
Alex Klein1699fab2022-09-08 08:46:06 -0600675 Subclasses can override this to provide better offset/length than what is
676 defined in the PGT partition layout.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800677
Alex Klein1699fab2022-09-08 08:46:06 -0600678 Args:
679 offset: The offset (in bytes) of the partition in the image.
680 length: The length (in bytes) of the partition.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800681
Alex Klein1699fab2022-09-08 08:46:06 -0600682 Returns:
683 A tuple of offset and length (in bytes) from the image.
684 """
685 return offset, length
Amin Hassanid684e982021-02-26 11:10:58 -0800686
Alex Klein1699fab2022-09-08 08:46:06 -0600687 def _RedirectPartition(self, file_name: str):
688 """Downloads the partition from a remote path and writes it into target.
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800689
Alex Klein1699fab2022-09-08 08:46:06 -0600690 Args:
691 file_name: The file name in the remote directory self._image.
692 """
693 image_path = os.path.join(self._image, file_name)
694 with GsFileCopier(image_path) as generator:
695 try:
696 with open(generator.Target(), "rb") as fp:
697 # Always use GZIP as remote quick provision images are gzip
698 # compressed only.
699 self._WriteToTarget(
700 fp,
Mike Frysinger66306012022-04-22 15:23:13 -0400701 self._device.GetDecompressor(
702 cros_build_lib.CompressionType.GZIP
703 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600704 )
705 finally:
706 generator.CloseTarget()
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800707
Amin Hassanid684e982021-02-26 11:10:58 -0800708
709class KernelUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600710 """A class to update the kernel partition on a Chromium OS device."""
Amin Hassanid684e982021-02-26 11:10:58 -0800711
Alex Klein1699fab2022-09-08 08:46:06 -0600712 def _GetPartitionName(self):
713 """See RawPartitionUpdater._GetPartitionName()."""
714 return constants.PART_KERN_B
Amin Hassanid684e982021-02-26 11:10:58 -0800715
Alex Klein1699fab2022-09-08 08:46:06 -0600716 def _GetRemotePartitionName(self):
717 """See RawPartitionUpdater._GetRemotePartitionName()."""
718 return constants.QUICK_PROVISION_PAYLOAD_KERNEL
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800719
Alex Klein1699fab2022-09-08 08:46:06 -0600720 def Revert(self):
721 """Reverts the kernel partition update."""
722 # There is nothing to do for reverting kernel partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800723
724
725class RootfsUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600726 """A class to update the root partition on a Chromium OS device."""
Amin Hassani75c5f942021-02-20 23:56:53 -0800727
Alex Klein1699fab2022-09-08 08:46:06 -0600728 def __init__(self, current_root: str, *args):
729 """Initializes the class.
Amin Hassani75c5f942021-02-20 23:56:53 -0800730
Alex Klein1699fab2022-09-08 08:46:06 -0600731 Args:
732 current_root: The current root device path.
733 *args: See PartitionUpdaterBase
734 """
735 super().__init__(*args)
Amin Hassani75c5f942021-02-20 23:56:53 -0800736
Alex Klein1699fab2022-09-08 08:46:06 -0600737 self._current_root = current_root
738 self._ran_postinst = False
Amin Hassani75c5f942021-02-20 23:56:53 -0800739
Alex Klein1699fab2022-09-08 08:46:06 -0600740 def _GetPartitionName(self):
741 """See RawPartitionUpdater._GetPartitionName()."""
742 return constants.PART_ROOT_A
Amin Hassani75c5f942021-02-20 23:56:53 -0800743
Alex Klein1699fab2022-09-08 08:46:06 -0600744 def _GetRemotePartitionName(self):
745 """See RawPartitionUpdater._GetRemotePartitionName()."""
746 return constants.QUICK_PROVISION_PAYLOAD_ROOTFS
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800747
Alex Klein1699fab2022-09-08 08:46:06 -0600748 def _Run(self):
749 """The function that does the job of rootfs partition update."""
750 with ProgressWatcher(self._device, self._target):
751 super()._Run()
Amin Hassani75c5f942021-02-20 23:56:53 -0800752
Alex Klein1699fab2022-09-08 08:46:06 -0600753 self._RunPostInst()
Amin Hassani75c5f942021-02-20 23:56:53 -0800754
Alex Klein1699fab2022-09-08 08:46:06 -0600755 def _OptimizePartLocation(self, offset: int, length: int):
756 """Optimizes the size of the root partition of the image.
Amin Hassani75c5f942021-02-20 23:56:53 -0800757
Alex Klein1699fab2022-09-08 08:46:06 -0600758 Normally the file system does not occupy the entire partition. Furthermore
759 we don't need the verity hash tree at the end of the root file system
760 because postinst will recreate it. This function reads the (approximate)
761 superblock of the ext4 partition and extracts the actual file system size in
762 the root partition.
763 """
764 superblock_size = 4096 * 2
765 with open(self._image, "rb") as r:
766 r.seek(offset)
767 with tempfile.NamedTemporaryFile(delete=False) as fp:
768 fp.write(r.read(superblock_size))
769 fp.close()
770 return offset, partition_lib.Ext2FileSystemSize(fp.name)
Amin Hassani75c5f942021-02-20 23:56:53 -0800771
Alex Klein1699fab2022-09-08 08:46:06 -0600772 def _RunPostInst(self, on_target: bool = True):
773 """Runs the postinst process in the root partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800774
Alex Klein1699fab2022-09-08 08:46:06 -0600775 Args:
776 on_target: If true the postinst is run on the target (inactive)
777 partition. This is used when doing normal updates. If false, the
778 postinst is run on the current (active) partition. This is used when
779 reverting an update.
780 """
781 try:
782 postinst_dir = "/"
783 partition = self._current_root
784 if on_target:
785 postinst_dir = self._device.run(
786 ["mktemp", "-d", "-p", self._device.work_dir],
787 capture_output=True,
788 ).stdout.strip()
789 self._device.run(
790 ["mount", "-o", "ro", self._target, postinst_dir]
791 )
792 partition = self._target
Amin Hassani75c5f942021-02-20 23:56:53 -0800793
Alex Klein1699fab2022-09-08 08:46:06 -0600794 self._ran_postinst = True
795 postinst = os.path.join(postinst_dir, "postinst")
796 result = self._device.run(
797 [postinst, partition], capture_output=True
798 )
Amin Hassani75c5f942021-02-20 23:56:53 -0800799
Alex Klein1699fab2022-09-08 08:46:06 -0600800 logging.debug(
801 "Postinst result on %s: \n%s", postinst, result.stdout
802 )
803 # DeviceImagerOperation will look for this log.
804 logging.info("Postinstall completed.")
805 finally:
806 if on_target:
807 self._device.run(["umount", postinst_dir])
Amin Hassani75c5f942021-02-20 23:56:53 -0800808
Alex Klein1699fab2022-09-08 08:46:06 -0600809 def Revert(self):
810 """Reverts the root update install."""
811 logging.info("Reverting the rootfs partition update.")
812 if self._ran_postinst:
813 # We don't have to do anything for revert if we haven't changed the kernel
814 # priorities yet.
815 self._RunPostInst(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800816
817
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000818class MiniOSUpdater(RawPartitionUpdater):
Alex Klein1699fab2022-09-08 08:46:06 -0600819 """A class to update the miniOS partition on a Chromium OS device."""
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000820
Alex Klein1699fab2022-09-08 08:46:06 -0600821 def __init__(self, *args):
822 """Initializes the class.
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000823
Alex Klein1699fab2022-09-08 08:46:06 -0600824 Args:
825 *args: See PartitionUpdaterBase
826 """
827 super().__init__(*args)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000828
Alex Klein1699fab2022-09-08 08:46:06 -0600829 self._ran_postinst = False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000830
Alex Klein1699fab2022-09-08 08:46:06 -0600831 def _GetPartitionName(self):
832 """See RawPartitionUpdater._GetPartitionName()."""
833 return constants.PART_MINIOS_A
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000834
Alex Klein1699fab2022-09-08 08:46:06 -0600835 def _GetRemotePartitionName(self):
836 """See RawPartitionUpdater._GetRemotePartitionName()."""
837 return constants.QUICK_PROVISION_PAYLOAD_MINIOS
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000838
Alex Klein1699fab2022-09-08 08:46:06 -0600839 def _Run(self):
840 """The function that does the job of rootfs partition update."""
841 if self._image_type == ImageType.FULL:
842 if self._MiniOSPartitionsExistInImage():
843 logging.info("Updating miniOS partition from local.")
844 super()._Run()
845 else:
846 logging.warning(
847 "Not updating miniOS partition as it does not exist."
848 )
849 return
850 elif self._image_type == ImageType.REMOTE_DIRECTORY:
851 if not gs.GSContext().Exists(
852 os.path.join(
853 self._image, constants.QUICK_PROVISION_PAYLOAD_MINIOS
854 )
855 ):
856 logging.warning("Not updating miniOS, missing remote files.")
857 return
858 elif not self._MiniOSPartitionsExist():
859 logging.warning("Not updating miniOS, missing partitions.")
860 return
861 else:
862 logging.info("Updating miniOS partition from remote.")
863 super()._Run()
864 else:
865 # Let super() handle this error.
866 super()._Run()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000867
Alex Klein1699fab2022-09-08 08:46:06 -0600868 self._RunPostInstall()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000869
Alex Klein1699fab2022-09-08 08:46:06 -0600870 def _RunPostInstall(self):
871 """The function will change the priority of the miniOS partitions."""
872 self._FlipMiniOSPriority()
873 self._ran_postinst = True
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000874
Alex Klein1699fab2022-09-08 08:46:06 -0600875 def Revert(self):
876 """Reverts the miniOS partition update."""
877 if self._ran_postinst:
878 self._FlipMiniOSPriority()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000879
Alex Klein1699fab2022-09-08 08:46:06 -0600880 def _GetMiniOSPriority(self):
881 return self._device.run(
882 ["crossystem", constants.MINIOS_PRIORITY]
883 ).stdout
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000884
Alex Klein1699fab2022-09-08 08:46:06 -0600885 def _SetMiniOSPriority(self, priority: str):
886 self._device.run(
887 ["crossystem", f"{constants.MINIOS_PRIORITY}={priority}"]
888 )
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000889
Alex Klein1699fab2022-09-08 08:46:06 -0600890 def _FlipMiniOSPriority(self):
891 inactive_minios_priority = (
892 "B" if self._GetMiniOSPriority() == "A" else "A"
893 )
894 logging.info("Setting miniOS priority to %s", inactive_minios_priority)
895 self._SetMiniOSPriority(inactive_minios_priority)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000896
Alex Klein1699fab2022-09-08 08:46:06 -0600897 def _MiniOSPartitionsExistInImage(self):
898 """Checks if miniOS partition exists in the image."""
899 d = cgpt.Disk.FromImage(self._image)
900 try:
901 d.GetPartitionByTypeGuid(cgpt.MINIOS_TYPE_GUID)
902 return True
903 except KeyError:
904 return False
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000905
Alex Klein1699fab2022-09-08 08:46:06 -0600906 def _MiniOSPartitionsExist(self):
907 """Checks if the device has miniOS partitions."""
908 run = lambda x: self._device.run(x).stdout.strip()
909 device_drive = run(["rootdev", "-s", "-d"])
910 cmd = ["cgpt", "show", "-t", device_drive, "-i"]
911 return all(
912 (run(cmd + [p]) == cgpt.MINIOS_TYPE_GUID) for p in ("9", "10")
913 )
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700914
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000915
Amin Hassani74403082021-02-22 11:40:09 -0800916class StatefulPayloadGenerator(ReaderBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600917 """A class for generating a stateful update payload in a separate thread."""
Amin Hassani74403082021-02-22 11:40:09 -0800918
Alex Klein1699fab2022-09-08 08:46:06 -0600919 def __init__(self, image: str):
920 """Initializes that class.
Amin Hassani74403082021-02-22 11:40:09 -0800921
Alex Klein1699fab2022-09-08 08:46:06 -0600922 Args:
923 image: The path to a local Chromium OS image.
924 """
925 super().__init__()
926 self._image = image
927
928 def run(self):
929 """Generates the stateful update and writes it into the output pipe."""
930 try:
931 paygen_stateful_payload_lib.GenerateStatefulPayload(
932 self._image, self._Source()
933 )
934 finally:
935 self._CloseSource()
Amin Hassani74403082021-02-22 11:40:09 -0800936
937
938class StatefulUpdater(PartitionUpdaterBase):
Alex Klein1699fab2022-09-08 08:46:06 -0600939 """A class to update the stateful partition on a device."""
Amin Hassani74403082021-02-22 11:40:09 -0800940
Alex Klein1699fab2022-09-08 08:46:06 -0600941 def __init__(self, clobber_stateful: bool, *args):
942 """Initializes the class
Amin Hassani74403082021-02-22 11:40:09 -0800943
Alex Klein1699fab2022-09-08 08:46:06 -0600944 Args:
945 clobber_stateful: Whether to clobber the stateful or not.
946 *args: Look at PartitionUpdaterBase.
947 """
948 super().__init__(*args)
949 self._clobber_stateful = clobber_stateful
Amin Hassani74403082021-02-22 11:40:09 -0800950
Alex Klein1699fab2022-09-08 08:46:06 -0600951 def _Run(self):
952 """Reads/Downloads the stateful updates and writes it into the device."""
953 if self._image_type == ImageType.FULL:
954 generator_cls = StatefulPayloadGenerator
955 elif self._image_type == ImageType.REMOTE_DIRECTORY:
956 generator_cls = GsFileCopier
957 self._image = os.path.join(
958 self._image, paygen_stateful_payload_lib.STATEFUL_FILE
959 )
960 else:
961 raise ValueError(f"Invalid image type {self._image_type}")
Amin Hassani74403082021-02-22 11:40:09 -0800962
Alex Klein1699fab2022-09-08 08:46:06 -0600963 with generator_cls(self._image) as generator:
964 try:
965 updater = stateful_updater.StatefulUpdater(self._device)
966 updater.Update(
967 generator.Target(),
968 is_payload_on_device=False,
969 update_type=(
970 stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER
971 if self._clobber_stateful
972 else None
973 ),
974 )
975 finally:
976 generator.CloseTarget()
977
978 def Revert(self):
979 """Reverts the stateful partition update."""
980 logging.info("Reverting the stateful update.")
981 stateful_updater.StatefulUpdater(self._device).Reset()
Amin Hassani55970562021-02-22 20:49:13 -0800982
983
984class ProgressWatcher(threading.Thread):
Alex Klein1699fab2022-09-08 08:46:06 -0600985 """A class used for watching the progress of rootfs update."""
Amin Hassani55970562021-02-22 20:49:13 -0800986
Alex Klein1699fab2022-09-08 08:46:06 -0600987 def __init__(self, device, target_root: str):
988 """Initializes the class.
Amin Hassani55970562021-02-22 20:49:13 -0800989
Alex Klein1699fab2022-09-08 08:46:06 -0600990 Args:
991 device: The ChromiumOSDevice to be updated.
992 target_root: The target root partition to monitor the progress of.
993 """
994 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -0800995
Alex Klein1699fab2022-09-08 08:46:06 -0600996 self._device = device
997 self._target_root = target_root
998 self._exit = False
Amin Hassani55970562021-02-22 20:49:13 -0800999
Alex Klein1699fab2022-09-08 08:46:06 -06001000 def __enter__(self):
1001 """Starts the thread."""
1002 self.start()
1003 return self
Amin Hassani55970562021-02-22 20:49:13 -08001004
Alex Klein1699fab2022-09-08 08:46:06 -06001005 def __exit__(self, *args, **kwargs):
1006 """Exists the thread."""
1007 self._exit = True
1008 self.join()
Amin Hassani55970562021-02-22 20:49:13 -08001009
Alex Klein1699fab2022-09-08 08:46:06 -06001010 def _ShouldExit(self):
1011 return self._exit
Amin Hassani55970562021-02-22 20:49:13 -08001012
Alex Klein1699fab2022-09-08 08:46:06 -06001013 def run(self):
1014 """Monitors the progress of the target root partitions' update.
Amin Hassani55970562021-02-22 20:49:13 -08001015
Alex Klein1699fab2022-09-08 08:46:06 -06001016 This is done by periodically, reading the fd position of the process that is
1017 writing into the target partition and reporting it back. Then the position
1018 is divided by the size of the block device to report an approximate
1019 progress.
1020 """
1021 cmd = ["blockdev", "--getsize64", self._target_root]
Amin Hassani55970562021-02-22 20:49:13 -08001022 output = self._device.run(cmd, capture_output=True).stdout.strip()
Alex Klein1699fab2022-09-08 08:46:06 -06001023 if output is None:
1024 raise Error(f"Cannot get the block device size from {output}.")
1025 dev_size = int(output)
1026
1027 # Using lsof to find out which process is writing to the target rootfs.
1028 cmd = ["lsof", "-t", self._target_root]
1029 while not self._ShouldExit():
1030 try:
1031 pid = self._device.run(cmd, capture_output=True).stdout.strip()
1032 if pid:
1033 break
1034 except cros_build_lib.RunCommandError:
1035 continue
1036 finally:
1037 time.sleep(1)
1038
1039 # Now that we know which process is writing to it, we can look the fdinfo of
1040 # stdout of that process to get its offset. We're assuming there will be no
1041 # seek, which is correct.
1042 cmd = ["cat", f"/proc/{pid}/fdinfo/1"]
1043 while not self._ShouldExit():
1044 try:
1045 output = self._device.run(
1046 cmd, capture_output=True
1047 ).stdout.strip()
1048 m = re.search(r"^pos:\s*(\d+)$", output, flags=re.M)
1049 if m:
1050 offset = int(m.group(1))
1051 # DeviceImagerOperation will look for this log.
1052 logging.info("RootFS progress: %f", offset / dev_size)
1053 except cros_build_lib.RunCommandError:
1054 continue
1055 finally:
1056 time.sleep(1)
Amin Hassani55970562021-02-22 20:49:13 -08001057
1058
1059class DeviceImagerOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -06001060 """A class to provide a progress bar for DeviceImager operation."""
Amin Hassani55970562021-02-22 20:49:13 -08001061
Alex Klein1699fab2022-09-08 08:46:06 -06001062 def __init__(self):
1063 """Initializes the class."""
1064 super().__init__()
Amin Hassani55970562021-02-22 20:49:13 -08001065
Alex Klein1699fab2022-09-08 08:46:06 -06001066 self._progress = 0.0
Amin Hassani55970562021-02-22 20:49:13 -08001067
Alex Klein1699fab2022-09-08 08:46:06 -06001068 def ParseOutput(self, output=None):
1069 """Override function to parse the output and provide progress.
Amin Hassani55970562021-02-22 20:49:13 -08001070
Alex Klein1699fab2022-09-08 08:46:06 -06001071 Args:
1072 output: The stderr or stdout.
1073 """
1074 output = self._stdout.read()
1075 match = re.findall(r"RootFS progress: (\d+(?:\.\d+)?)", output)
1076 if match:
1077 progress = float(match[0])
1078 self._progress = max(self._progress, progress)
Amin Hassani55970562021-02-22 20:49:13 -08001079
Alex Klein1699fab2022-09-08 08:46:06 -06001080 # If postinstall completes, move half of the remaining progress.
1081 if re.findall(r"Postinstall completed", output):
1082 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001083
Alex Klein1699fab2022-09-08 08:46:06 -06001084 # While waiting for reboot, each time, move half of the remaining progress.
1085 if re.findall(r"Unable to get new boot_id", output):
1086 self._progress += (1.0 - self._progress) / 2
Amin Hassani55970562021-02-22 20:49:13 -08001087
Alex Klein1699fab2022-09-08 08:46:06 -06001088 if re.findall(r"DeviceImager completed.", output):
1089 self._progress = 1.0
Amin Hassani55970562021-02-22 20:49:13 -08001090
Alex Klein1699fab2022-09-08 08:46:06 -06001091 self.ProgressBar(self._progress)