blob: 5fcd430db5ba2e82fb8e313a4aefad9bc3c07f2a [file] [log] [blame]
Amin Hassani92f6c4a2021-02-20 17:36:09 -08001# Copyright 2021 The Chromium OS Authors. All rights reserved.
2# 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
Chris McDonald14ac61d2021-07-21 11:49:56 -06009import logging
Amin Hassani92f6c4a2021-02-20 17:36:09 -080010import os
11import re
Amin Hassanid4b3ff82021-02-20 23:05:14 -080012import tempfile
13import threading
Amin Hassani55970562021-02-22 20:49:13 -080014import time
Chris McDonald14ac61d2021-07-21 11:49:56 -060015from typing import Dict, Tuple
Amin Hassani92f6c4a2021-02-20 17:36:09 -080016
Amin Hassani55970562021-02-22 20:49:13 -080017from chromite.cli import command
Amin Hassanicf8f0042021-03-12 10:42:13 -080018from chromite.cli import flash
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000019from chromite.lib import cgpt
Amin Hassanid4b3ff82021-02-20 23:05:14 -080020from chromite.lib import constants
Amin Hassani92f6c4a2021-02-20 17:36:09 -080021from chromite.lib import cros_build_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080022from chromite.lib import gs
Amin Hassanid4b3ff82021-02-20 23:05:14 -080023from chromite.lib import image_lib
Amin Hassani55970562021-02-22 20:49:13 -080024from chromite.lib import operation
Amin Hassanid4b3ff82021-02-20 23:05:14 -080025from chromite.lib import osutils
Amin Hassani92f6c4a2021-02-20 17:36:09 -080026from chromite.lib import parallel
27from chromite.lib import remote_access
28from chromite.lib import retry_util
Amin Hassani74403082021-02-22 11:40:09 -080029from chromite.lib import stateful_updater
Amin Hassani75c5f942021-02-20 23:56:53 -080030from chromite.lib.paygen import partition_lib
Amin Hassani74403082021-02-22 11:40:09 -080031from chromite.lib.paygen import paygen_stateful_payload_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080032from chromite.lib.xbuddy import devserver_constants
33from chromite.lib.xbuddy import xbuddy
Alex Klein18ef1212021-10-14 12:49:02 -060034from chromite.utils import timer
Amin Hassani92f6c4a2021-02-20 17:36:09 -080035
36
Amin Hassani92f6c4a2021-02-20 17:36:09 -080037class Error(Exception):
38 """Thrown when there is a general Chromium OS-specific flash error."""
39
40
41class ImageType(enum.Enum):
42 """Type of the image that is used for flashing the device."""
43
44 # The full image on disk (e.g. chromiumos_test_image.bin).
45 FULL = 0
46 # The remote directory path
47 # (e.g gs://chromeos-image-archive/eve-release/R90-x.x.x)
48 REMOTE_DIRECTORY = 1
49
50
51class Partition(enum.Enum):
52 """An enum for partition types like kernel and rootfs."""
53 KERNEL = 0
54 ROOTFS = 1
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000055 MINIOS = 2
Amin Hassani92f6c4a2021-02-20 17:36:09 -080056
57
58class DeviceImager(object):
59 """A class to flash a Chromium OS device.
60
61 This utility uses parallelism as much as possible to achieve its goal as fast
62 as possible. For example, it uses parallel compressors, parallel transfers,
63 and simultaneous pipes.
64 """
65
66 # The parameters of the kernel and rootfs's two main partitions.
67 A = {Partition.KERNEL: 2, Partition.ROOTFS: 3}
68 B = {Partition.KERNEL: 4, Partition.ROOTFS: 5}
69
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000070 MINIOS_A = {Partition.MINIOS: 9}
71 MINIOS_B = {Partition.MINIOS: 10}
72
Amin Hassani92f6c4a2021-02-20 17:36:09 -080073 def __init__(self, device, image: str,
Amin Hassanicf8f0042021-03-12 10:42:13 -080074 board: str = None,
75 version: str = None,
Amin Hassani92f6c4a2021-02-20 17:36:09 -080076 no_rootfs_update: bool = False,
77 no_stateful_update: bool = False,
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000078 no_minios_update: bool = False,
Amin Hassani92f6c4a2021-02-20 17:36:09 -080079 no_reboot: bool = False,
80 disable_verification: bool = False,
81 clobber_stateful: bool = False,
Daichi Hirono28831b3b2022-04-07 12:41:11 +090082 clear_tpm_owner: bool = False,
83 delta: bool = False):
Amin Hassani92f6c4a2021-02-20 17:36:09 -080084 """Initialize DeviceImager for flashing a Chromium OS device.
85
86 Args:
87 device: The ChromiumOSDevice to be updated.
88 image: The target image path (can be xBuddy path).
Amin Hassanicf8f0042021-03-12 10:42:13 -080089 board: Board to use.
90 version: Image version to use.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080091 no_rootfs_update: Whether to do rootfs partition update.
92 no_stateful_update: Whether to do stateful partition update.
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000093 no_minios_update: Whether to do minios partition update.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080094 no_reboot: Whether to reboot device after update. The default is True.
95 disable_verification: Whether to disabling rootfs verification on the
96 device.
97 clobber_stateful: Whether to do a clean stateful partition.
98 clear_tpm_owner: If true, it will clear the TPM owner on reboot.
Daichi Hirono28831b3b2022-04-07 12:41:11 +090099 delta: Whether to use delta compression when transferring image bytes.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800100 """
101
102 self._device = device
103 self._image = image
Amin Hassanicf8f0042021-03-12 10:42:13 -0800104 self._board = board
105 self._version = version
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800106 self._no_rootfs_update = no_rootfs_update
107 self._no_stateful_update = no_stateful_update
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000108 self._no_minios_update = no_minios_update
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800109 self._no_reboot = no_reboot
110 self._disable_verification = disable_verification
111 self._clobber_stateful = clobber_stateful
112 self._clear_tpm_owner = clear_tpm_owner
113
Amin Hassanib1993eb2021-04-28 12:00:11 -0700114 self._image_type = None
Amin Hassani0972d392021-03-31 19:04:19 -0700115 self._compression = cros_build_lib.COMP_GZIP
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800116 self._inactive_state = None
Daichi Hirono28831b3b2022-04-07 12:41:11 +0900117 self._delta = delta
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800118
119 def Run(self):
120 """Update the device with image of specific version."""
Amin Hassanib1993eb2021-04-28 12:00:11 -0700121 self._LocateImage()
122 logging.notice('Preparing to update the remote device %s with image %s',
123 self._device.hostname, self._image)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800124
125 try:
Amin Hassani55970562021-02-22 20:49:13 -0800126 if command.UseProgressBar():
127 op = DeviceImagerOperation()
128 op.Run(self._Run)
129 else:
130 self._Run()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800131 except Exception as e:
132 raise Error(f'DeviceImager Failed with error: {e}')
133
Amin Hassani55970562021-02-22 20:49:13 -0800134 # DeviceImagerOperation will look for this log.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800135 logging.info('DeviceImager completed.')
136
137 def _Run(self):
138 """Runs the various operations to install the image on device."""
Amin Hassanib1993eb2021-04-28 12:00:11 -0700139 # Override the compression as remote quick provision images are gzip
140 # compressed only.
141 if self._image_type == ImageType.REMOTE_DIRECTORY:
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800142 self._compression = cros_build_lib.COMP_GZIP
143
Daichi Hirono28831b3b2022-04-07 12:41:11 +0900144 # TODO(b/228389041): Switch to delta compression if self._delta is True
145
Amin Hassanib1993eb2021-04-28 12:00:11 -0700146 self._InstallPartitions()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800147
148 if self._clear_tpm_owner:
149 self._device.ClearTpmOwner()
150
151 if not self._no_reboot:
152 self._Reboot()
153 self._VerifyBootExpectations()
154
155 if self._disable_verification:
156 self._device.DisableRootfsVerification()
157
Amin Hassanib1993eb2021-04-28 12:00:11 -0700158 def _LocateImage(self):
159 """Locates the path to the final image(s) that need to be installed.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800160
161 If the paths is local, the image should be the Chromium OS GPT image
162 (e.g. chromiumos_test_image.bin). If the path is remote, it should be the
163 remote directory where we can find the quick-provision and stateful update
164 files (e.g. gs://chromeos-image-archive/eve-release/R90-x.x.x).
165
166 NOTE: At this point there is no caching involved. Hence we always download
167 the partition payloads or extract them from the Chromium OS image.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800168 """
169 if os.path.isfile(self._image):
Amin Hassanib1993eb2021-04-28 12:00:11 -0700170 self._image_type = ImageType.FULL
171 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800172
173 # TODO(b/172212406): We could potentially also allow this by searching
174 # through the directory to see whether we have quick-provision and stateful
175 # payloads. This only makes sense when a user has their workstation at home
176 # and doesn't want to incur the bandwidth cost of downloading the same
177 # image multiple times. For that, they can simply download the GPT image
178 # image first and flash that instead.
179 if os.path.isdir(self._image):
180 raise ValueError(
181 f'{self._image}: input must be a disk image, not a directory.')
182
183 if gs.PathIsGs(self._image):
184 # TODO(b/172212406): Check whether it is a directory. If it wasn't a
185 # directory download the image into some temp location and use it instead.
Amin Hassanib1993eb2021-04-28 12:00:11 -0700186 self._image_type = ImageType.REMOTE_DIRECTORY
187 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800188
189 # Assuming it is an xBuddy path.
Amin Hassanicf8f0042021-03-12 10:42:13 -0800190 board = cros_build_lib.GetBoard(
191 device_board=self._device.board or flash.GetDefaultBoard(),
192 override_board=self._board, force=True)
193
Amin Hassania20d20d2021-04-28 10:18:18 -0700194 xb = xbuddy.XBuddy(board=board, version=self._version)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800195 build_id, local_file = xb.Translate([self._image])
196 if build_id is None:
197 raise Error(f'{self._image}: unable to find matching xBuddy path.')
198 logging.info('XBuddy path translated to build ID %s', build_id)
199
200 if local_file:
Amin Hassanib1993eb2021-04-28 12:00:11 -0700201 self._image = local_file
202 self._image_type = ImageType.FULL
203 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800204
Amin Hassanib1993eb2021-04-28 12:00:11 -0700205 self._image = f'{devserver_constants.GS_IMAGE_DIR}/{build_id}'
206 self._image_type = ImageType.REMOTE_DIRECTORY
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800207
208 def _SplitDevPath(self, path: str) -> Tuple[str, int]:
209 """Splits the given /dev/x path into prefix and the dev number.
210
211 Args:
212 path: The path to a block dev device.
213
214 Returns:
215 A tuple of representing the prefix and the index of the dev path.
216 e.g.: '/dev/mmcblk0p1' -> ['/dev/mmcblk0p', 1]
217 """
218 match = re.search(r'(.*)([0-9]+)$', path)
219 if match is None:
220 raise Error(f'{path}: Could not parse root dev path.')
221
222 return match.group(1), int(match.group(2))
223
224 def _GetKernelState(self, root_num: int) -> Tuple[Dict, Dict]:
225 """Returns the kernel state.
226
227 Returns:
228 A tuple of two dictionaries: The current active kernel state and the
229 inactive kernel state. (Look at A and B constants in this class.)
230 """
231 if root_num == self.A[Partition.ROOTFS]:
232 return self.A, self.B
233 elif root_num == self.B[Partition.ROOTFS]:
234 return self.B, self.A
235 else:
236 raise Error(f'Invalid root partition number {root_num}')
237
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000238 def _GetMiniOSState(self, minios_num: int) -> Tuple[Dict, Dict]:
239 """Returns the miniOS state.
240
241 Returns:
242 A tuple of dictionaries: The current active miniOS state and the inactive
243 miniOS state.
244 """
245 if minios_num == self.MINIOS_A[Partition.MINIOS]:
246 return self.MINIOS_A, self.MINIOS_B
247 elif minios_num == self.MINIOS_B[Partition.MINIOS]:
248 return self.MINIOS_B, self.MINIOS_A
249 else:
250 raise Error(f'Invalid minios partition number {minios_num}')
251
Amin Hassanib1993eb2021-04-28 12:00:11 -0700252 def _InstallPartitions(self):
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800253 """The main method that installs the partitions of a Chrome OS device.
254
255 It uses parallelism to install the partitions as fast as possible.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800256 """
Amin Hassanid684e982021-02-26 11:10:58 -0800257 prefix, root_num = self._SplitDevPath(self._device.root_dev)
258 active_state, self._inactive_state = self._GetKernelState(root_num)
259
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800260 updaters = []
Amin Hassanid684e982021-02-26 11:10:58 -0800261 if not self._no_rootfs_update:
Amin Hassani75c5f942021-02-20 23:56:53 -0800262 current_root = prefix + str(active_state[Partition.ROOTFS])
263 target_root = prefix + str(self._inactive_state[Partition.ROOTFS])
Amin Hassanib1993eb2021-04-28 12:00:11 -0700264 updaters.append(RootfsUpdater(current_root, self._device, self._image,
265 self._image_type, target_root,
266 self._compression))
Amin Hassani75c5f942021-02-20 23:56:53 -0800267
Amin Hassanid684e982021-02-26 11:10:58 -0800268 target_kernel = prefix + str(self._inactive_state[Partition.KERNEL])
Amin Hassanib1993eb2021-04-28 12:00:11 -0700269 updaters.append(KernelUpdater(self._device, self._image, self._image_type,
Amin Hassanid684e982021-02-26 11:10:58 -0800270 target_kernel, self._compression))
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800271
Amin Hassani74403082021-02-22 11:40:09 -0800272 if not self._no_stateful_update:
273 updaters.append(StatefulUpdater(self._clobber_stateful, self._device,
Amin Hassanib1993eb2021-04-28 12:00:11 -0700274 self._image, self._image_type, None,
275 None))
Amin Hassani74403082021-02-22 11:40:09 -0800276
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000277 if not self._no_minios_update:
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700278 minios_priority = self._device.run(
279 ['crossystem', constants.MINIOS_PRIORITY]).stdout
280 if minios_priority not in ['A', 'B']:
281 logging.warning('Skipping miniOS flash due to missing priority.')
282 else:
283 # Reference disk_layout_v3 for partition numbering.
284 _, inactive_minios_state = self._GetMiniOSState(
285 9 if minios_priority == 'A' else 10)
286 target_minios = prefix + str(inactive_minios_state[Partition.MINIOS])
287 minios_updater = MiniOSUpdater(self._device, self._image,
288 self._image_type, target_minios,
289 self._compression)
290 updaters.append(minios_updater)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000291
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800292 # Retry the partitions updates that failed, in case a transient error (like
293 # SSH drop, etc) caused the error.
294 num_retries = 1
295 try:
296 retry_util.RetryException(Error, num_retries,
297 parallel.RunParallelSteps,
298 (x.Run for x in updaters if not x.IsFinished()),
299 halt_on_error=True)
300 except Exception:
301 # If one of the partitions failed to be installed, revert all partitions.
302 parallel.RunParallelSteps(x.Revert for x in updaters)
303 raise
304
305 def _Reboot(self):
306 """Reboots the device."""
307 try:
Amin Hassani9281b682021-03-08 16:38:25 -0800308 self._device.Reboot(timeout_sec=300)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800309 except remote_access.RebootError:
310 raise Error('Could not recover from reboot. Once example reason'
311 ' could be the image provided was a non-test image'
312 ' or the system failed to boot after the update.')
313 except Exception as e:
314 raise Error(f'Failed to reboot to the device with error: {e}')
315
316 def _VerifyBootExpectations(self):
317 """Verify that we fully booted into the expected kernel state."""
318 # Discover the newly active kernel.
319 _, root_num = self._SplitDevPath(self._device.root_dev)
320 active_state, _ = self._GetKernelState(root_num)
321
322 # If this happens, we should rollback.
323 if active_state != self._inactive_state:
324 raise Error('The expected kernel state after update is invalid.')
325
326 logging.info('Verified boot expectations.')
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800327
328
329class ReaderBase(threading.Thread):
330 """The base class for reading different inputs and writing into output.
331
332 This class extends threading.Thread, so it will be run on its own thread. Also
333 it can be used as a context manager. Internally, it opens necessary files for
334 writing to and reading from. This class cannot be instantiated, it needs to be
335 sub-classed first to provide necessary function implementations.
336 """
337
338 def __init__(self, use_named_pipes: bool = False):
339 """Initializes the class.
340
341 Args:
342 use_named_pipes: Whether to use a named pipe or anonymous file
343 descriptors.
344 """
345 super().__init__()
346 self._use_named_pipes = use_named_pipes
347 self._pipe_target = None
348 self._pipe_source = None
349
350 def __del__(self):
351 """Destructor.
352
353 Make sure to clean up any named pipes we might have created.
354 """
355 if self._use_named_pipes:
356 osutils.SafeUnlink(self._pipe_target)
357
358 def __enter__(self):
359 """Enters the context manager"""
360 if self._use_named_pipes:
361 # There is no need for the temp file, we only need its path. So the named
362 # pipe is created after this temp file is deleted.
363 with tempfile.NamedTemporaryFile(prefix='chromite-device-imager') as fp:
364 self._pipe_target = self._pipe_source = fp.name
365 os.mkfifo(self._pipe_target)
366 else:
367 self._pipe_target, self._pipe_source = os.pipe()
368
369 self.start()
370 return self
371
372 def __exit__(self, *args, **kwargs):
373 """Exits the context manager."""
374 self.join()
375
376 def _Source(self):
377 """Returns the source pipe to write data into.
378
379 Sub-classes can use this function to determine where to write their data
380 into.
381 """
382 return self._pipe_source
383
384 def _CloseSource(self):
385 """Closes the source pipe.
386
387 Sub-classes should use this function to close the pipe after they are done
388 writing into it. Failure to do so may result reader of the data to hang
389 indefinitely.
390 """
391 if not self._use_named_pipes:
392 os.close(self._pipe_source)
393
394 def Target(self):
395 """Returns the target pipe to read data from.
396
397 Users of this class can use this path to read data from.
398 """
399 return self._pipe_target
400
401 def CloseTarget(self):
402 """Closes the target pipe.
403
404 Users of this class should use this function to close the pipe after they
405 are done reading from it.
406 """
407 if self._use_named_pipes:
408 os.remove(self._pipe_target)
409 else:
410 os.close(self._pipe_target)
411
412
413class PartialFileReader(ReaderBase):
414 """A class to read specific offset and length from a file and compress it.
415
416 This class can be used to read from specific location and length in a file
417 (e.g. A partition in a GPT image). Then it compresses the input and writes it
418 out (to a pipe). Look at the base class for more information.
419 """
420
421 # The offset of different partitions in a Chromium OS image does not always
422 # align to larger values like 4096. It seems that 512 is the maximum value to
423 # be divisible by partition offsets. This size should not be increased just
424 # for 'performance reasons'. Since we are doing everything in parallel, in
425 # practice there is not much difference between this and larger block sizes as
426 # parallelism hides the possible extra latency provided by smaller block
427 # sizes.
428 _BLOCK_SIZE = 512
429
430 def __init__(self, image: str, offset: int, length: int, compression):
431 """Initializes the class.
432
433 Args:
434 image: The path to an image (local or remote directory).
435 offset: The offset (in bytes) to read from the image.
436 length: The length (in bytes) to read from the image.
437 compression: The compression type (see cros_build_lib.COMP_XXX).
438 """
439 super().__init__()
440
441 self._image = image
442 self._offset = offset
443 self._length = length
444 self._compression = compression
445
446 def run(self):
447 """Runs the reading and compression."""
448 cmd = [
449 'dd',
450 'status=none',
451 f'if={self._image}',
452 f'ibs={self._BLOCK_SIZE}',
453 f'skip={int(self._offset/self._BLOCK_SIZE)}',
454 f'count={int(self._length/self._BLOCK_SIZE)}',
455 '|',
456 cros_build_lib.FindCompressor(self._compression),
457 ]
458
459 try:
460 cros_build_lib.run(' '.join(cmd), stdout=self._Source(), shell=True)
461 finally:
462 self._CloseSource()
463
464
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800465class GsFileCopier(ReaderBase):
466 """A class for downloading gzip compressed file from GS bucket into a pipe."""
467
468 def __init__(self, image: str):
469 """Initializes the class.
470
471 Args:
472 image: The path to an image (local or remote directory).
473 """
474 super().__init__(use_named_pipes=True)
475 self._image = image
476
477 def run(self):
478 """Runs the download and write into the output pipe."""
479 try:
480 gs.GSContext().Copy(self._image, self._Source())
481 finally:
482 self._CloseSource()
483
484
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800485class PartitionUpdaterBase(object):
486 """A base abstract class to use for installing an image into a partition.
487
488 Sub-classes should implement the abstract methods to provide the core
489 functionality.
490 """
491 def __init__(self, device, image: str, image_type, target: str, compression):
492 """Initializes this base class with values that most sub-classes will need.
493
494 Args:
495 device: The ChromiumOSDevice to be updated.
496 image: The target image path for the partition update.
497 image_type: The type of the image (ImageType).
498 target: The target path (e.g. block dev) to install the update.
499 compression: The compression used for compressing the update payload.
500 """
501 self._device = device
502 self._image = image
503 self._image_type = image_type
504 self._target = target
505 self._compression = compression
506 self._finished = False
507
508 def Run(self):
509 """The main function that does the partition update job."""
Alex Klein18ef1212021-10-14 12:49:02 -0600510 with timer.Timer() as t:
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800511 try:
512 self._Run()
513 finally:
514 self._finished = True
515
Alex Klein18ef1212021-10-14 12:49:02 -0600516 logging.debug('Completed %s in %s', self.__class__.__name__, t)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800517
518 @abc.abstractmethod
519 def _Run(self):
520 """The method that need to be implemented by sub-classes."""
521 raise NotImplementedError('Sub-classes need to implement this.')
522
523 def IsFinished(self):
524 """Returns whether the partition update has been successful."""
525 return self._finished
526
527 @abc.abstractmethod
528 def Revert(self):
529 """Reverts the partition update.
530
531 Sub-classes need to implement this function to provide revert capability.
532 """
533 raise NotImplementedError('Sub-classes need to implement this.')
534
535
536class RawPartitionUpdater(PartitionUpdaterBase):
537 """A class to update a raw partition on a Chromium OS device."""
538
539 def _Run(self):
540 """The function that does the job of kernel partition update."""
541 if self._image_type == ImageType.FULL:
542 self._CopyPartitionFromImage(self._GetPartitionName())
543 elif self._image_type == ImageType.REMOTE_DIRECTORY:
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800544 self._RedirectPartition(self._GetRemotePartitionName())
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800545 else:
546 raise ValueError(f'Invalid image type {self._image_type}')
547
548 def _GetPartitionName(self):
549 """Returns the name of the partition in a Chromium OS GPT layout.
550
551 Subclasses should override this function to return correct name.
552 """
553 raise NotImplementedError('Subclasses need to implement this.')
554
555 def _CopyPartitionFromImage(self, part_name: str):
556 """Updates the device's partition from a local Chromium OS image.
557
558 Args:
559 part_name: The name of the partition in the source image that needs to be
560 extracted.
561 """
562 cmd = self._GetWriteToTargetCommand()
563
564 offset, length = self._GetPartLocation(part_name)
565 offset, length = self._OptimizePartLocation(offset, length)
566 with PartialFileReader(self._image, offset, length,
567 self._compression) as generator:
568 try:
569 self._device.run(cmd, input=generator.Target(), shell=True)
570 finally:
571 generator.CloseTarget()
572
573 def _GetWriteToTargetCommand(self):
574 """Returns a write to target command to run on a Chromium OS device.
575
576 Returns:
577 A string command to run on a device to read data from stdin, uncompress it
578 and write it to the target partition.
579 """
580 cmd = self._device.GetDecompressor(self._compression)
581 # Using oflag=direct to tell the OS not to cache the writes (faster).
582 cmd += ['|', 'dd', 'bs=1M', 'oflag=direct', f'of={self._target}']
583 return ' '.join(cmd)
584
585 def _GetPartLocation(self, part_name: str):
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000586 """Extracts the location and size of the raw partition from the image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800587
588 Args:
589 part_name: The name of the partition in the source image that needs to be
590 extracted.
591
592 Returns:
593 A tuple of offset and length (in bytes) from the image.
594 """
595 try:
596 parts = image_lib.GetImageDiskPartitionInfo(self._image)
597 part_info = [p for p in parts if p.name == part_name][0]
598 except IndexError:
599 raise Error(f'No partition named {part_name} found.')
600
601 return int(part_info.start), int(part_info.size)
602
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800603 def _GetRemotePartitionName(self):
604 """Returns the name of the quick-provision partition file.
605
606 Subclasses should override this function to return correct name.
607 """
608 raise NotImplementedError('Subclasses need to implement this.')
609
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800610 def _OptimizePartLocation(self, offset: int, length: int):
611 """Optimizes the offset and length of the partition.
612
613 Subclasses can override this to provide better offset/length than what is
614 defined in the PGT partition layout.
615
616 Args:
617 offset: The offset (in bytes) of the partition in the image.
618 length: The length (in bytes) of the partition.
619
620 Returns:
621 A tuple of offset and length (in bytes) from the image.
622 """
623 return offset, length
Amin Hassanid684e982021-02-26 11:10:58 -0800624
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800625 def _RedirectPartition(self, file_name: str):
626 """Downloads the partition from a remote path and writes it into target.
627
628 Args:
629 file_name: The file name in the remote directory self._image.
630 """
631 cmd = self._GetWriteToTargetCommand()
632
633 image_path = os.path.join(self._image, file_name)
634 with GsFileCopier(image_path) as generator:
635 try:
636 with open(generator.Target(), 'rb') as fp:
637 self._device.run(cmd, input=fp, shell=True)
638 finally:
639 generator.CloseTarget()
640
Amin Hassanid684e982021-02-26 11:10:58 -0800641
642class KernelUpdater(RawPartitionUpdater):
643 """A class to update the kernel partition on a Chromium OS device."""
644
645 def _GetPartitionName(self):
646 """See RawPartitionUpdater._GetPartitionName()."""
647 return constants.PART_KERN_B
648
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800649 def _GetRemotePartitionName(self):
650 """See RawPartitionUpdater._GetRemotePartitionName()."""
651 return constants.QUICK_PROVISION_PAYLOAD_KERNEL
652
Amin Hassanid684e982021-02-26 11:10:58 -0800653 def Revert(self):
654 """Reverts the kernel partition update."""
655 # There is nothing to do for reverting kernel partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800656
657
658class RootfsUpdater(RawPartitionUpdater):
659 """A class to update the root partition on a Chromium OS device."""
660
661 def __init__(self, current_root: str, *args):
662 """Initializes the class.
663
664 Args:
665 current_root: The current root device path.
666 *args: See PartitionUpdaterBase
667 """
668 super().__init__(*args)
669
670 self._current_root = current_root
671 self._ran_postinst = False
672
673 def _GetPartitionName(self):
674 """See RawPartitionUpdater._GetPartitionName()."""
675 return constants.PART_ROOT_A
676
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800677 def _GetRemotePartitionName(self):
678 """See RawPartitionUpdater._GetRemotePartitionName()."""
679 return constants.QUICK_PROVISION_PAYLOAD_ROOTFS
680
Amin Hassani75c5f942021-02-20 23:56:53 -0800681 def _Run(self):
682 """The function that does the job of rootfs partition update."""
Amin Hassani55970562021-02-22 20:49:13 -0800683 with ProgressWatcher(self._device, self._target):
684 super()._Run()
Amin Hassani75c5f942021-02-20 23:56:53 -0800685
686 self._RunPostInst()
687
688 def _OptimizePartLocation(self, offset: int, length: int):
689 """Optimizes the size of the root partition of the image.
690
691 Normally the file system does not occupy the entire partition. Furthermore
692 we don't need the verity hash tree at the end of the root file system
693 because postinst will recreate it. This function reads the (approximate)
694 superblock of the ext4 partition and extracts the actual file system size in
695 the root partition.
696 """
697 superblock_size = 4096 * 2
698 with open(self._image, 'rb') as r:
699 r.seek(offset)
700 with tempfile.NamedTemporaryFile(delete=False) as fp:
701 fp.write(r.read(superblock_size))
702 fp.close()
703 return offset, partition_lib.Ext2FileSystemSize(fp.name)
704
705 def _RunPostInst(self, on_target: bool = True):
706 """Runs the postinst process in the root partition.
707
708 Args:
709 on_target: If true the postinst is run on the target (inactive)
710 partition. This is used when doing normal updates. If false, the
711 postinst is run on the current (active) partition. This is used when
712 reverting an update.
713 """
714 try:
715 postinst_dir = '/'
716 partition = self._current_root
717 if on_target:
718 postinst_dir = self._device.run(
719 ['mktemp', '-d', '-p', self._device.work_dir],
720 capture_output=True).stdout.strip()
721 self._device.run(['mount', '-o', 'ro', self._target, postinst_dir])
722 partition = self._target
723
724 self._ran_postinst = True
725 postinst = os.path.join(postinst_dir, 'postinst')
726 result = self._device.run([postinst, partition], capture_output=True)
727
728 logging.debug('Postinst result on %s: \n%s', postinst, result.stdout)
Amin Hassani55970562021-02-22 20:49:13 -0800729 # DeviceImagerOperation will look for this log.
Amin Hassani75c5f942021-02-20 23:56:53 -0800730 logging.info('Postinstall completed.')
731 finally:
732 if on_target:
733 self._device.run(['umount', postinst_dir])
734
735 def Revert(self):
736 """Reverts the root update install."""
737 logging.info('Reverting the rootfs partition update.')
738 if self._ran_postinst:
739 # We don't have to do anything for revert if we haven't changed the kernel
740 # priorities yet.
741 self._RunPostInst(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800742
743
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000744class MiniOSUpdater(RawPartitionUpdater):
745 """A class to update the miniOS partition on a Chromium OS device."""
746
747 def __init__(self, *args):
748 """Initializes the class.
749
750 Args:
751 *args: See PartitionUpdaterBase
752 """
753 super().__init__(*args)
754
755 self._ran_postinst = False
756
757 def _GetPartitionName(self):
758 """See RawPartitionUpdater._GetPartitionName()."""
759 return constants.PART_MINIOS_A
760
761 def _GetRemotePartitionName(self):
762 """See RawPartitionUpdater._GetRemotePartitionName()."""
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700763 return constants.QUICK_PROVISION_PAYLOAD_MINIOS
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000764
765 def _Run(self):
766 """The function that does the job of rootfs partition update."""
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700767 if self._image_type == ImageType.FULL:
768 if self._MiniOSPartitionsExistInImage():
769 logging.info('Updating miniOS partition from local.')
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000770 super()._Run()
771 else:
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700772 logging.warning('Not updating miniOS partition as it does not exist.')
773 return
774 elif self._image_type == ImageType.REMOTE_DIRECTORY:
775 if not gs.GSContext().Exists(
776 os.path.join(self._image,
777 constants.QUICK_PROVISION_PAYLOAD_MINIOS)):
778 logging.warning('Not updating miniOS, missing remote files.')
779 return
780 elif not self._MiniOSPartitionsExist():
781 logging.warning('Not updating miniOS, missing partitions.')
782 return
783 else:
784 logging.info('Updating miniOS partition from remote.')
785 super()._Run()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000786 else:
787 # Let super() handle this error.
788 super()._Run()
789
790 self._RunPostInstall()
791
792 def _RunPostInstall(self):
793 """The function will change the priority of the miniOS partitions."""
794 self._FlipMiniOSPriority()
795 self._ran_postinst = True
796
797 def Revert(self):
798 """Reverts the miniOS partition update."""
799 if self._ran_postinst:
800 self._FlipMiniOSPriority()
801
802 def _GetMiniOSPriority(self):
803 return self._device.run(['crossystem', constants.MINIOS_PRIORITY]).output
804
805 def _SetMiniOSPriority(self, priority: str):
806 self._device.run(
807 ['crossystem', f'{constants.MINIOS_PRIORITY}={priority}'])
808
809 def _FlipMiniOSPriority(self):
810 inactive_minios_priority = 'B' if self._GetMiniOSPriority() == 'A' else 'A'
811 logging.info('Setting miniOS priority to %s', inactive_minios_priority)
812 self._SetMiniOSPriority(inactive_minios_priority)
813
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700814 def _MiniOSPartitionsExistInImage(self):
815 """Checks if miniOS partition exists in the image."""
816 d = cgpt.Disk.FromImage(self._image)
817 try:
818 d.GetPartitionByTypeGuid(cgpt.MINIOS_TYPE_GUID)
819 return True
820 except KeyError:
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000821 return False
822
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700823 def _MiniOSPartitionsExist(self):
824 """Checks if the device has miniOS partitions."""
825 run = lambda x: self._device.run(x).stdout.strip()
826 device_drive = run(['rootdev', '-s', '-d'])
827 cmd = ['cgpt', 'show', '-t', device_drive, '-i']
828 return all((run(cmd + [p]) == cgpt.MINIOS_TYPE_GUID) for p in ('9', '10'))
829
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000830
Amin Hassani74403082021-02-22 11:40:09 -0800831class StatefulPayloadGenerator(ReaderBase):
832 """A class for generating a stateful update payload in a separate thread."""
833 def __init__(self, image: str):
834 """Initializes that class.
835
836 Args:
837 image: The path to a local Chromium OS image.
838 """
839 super().__init__()
840 self._image = image
841
842 def run(self):
843 """Generates the stateful update and writes it into the output pipe."""
844 try:
845 paygen_stateful_payload_lib.GenerateStatefulPayload(
846 self._image, self._Source())
847 finally:
848 self._CloseSource()
849
850
851class StatefulUpdater(PartitionUpdaterBase):
852 """A class to update the stateful partition on a device."""
853 def __init__(self, clobber_stateful: bool, *args):
854 """Initializes the class
855
856 Args:
857 clobber_stateful: Whether to clobber the stateful or not.
858 *args: Look at PartitionUpdaterBase.
859 """
860 super().__init__(*args)
861 self._clobber_stateful = clobber_stateful
862
863 def _Run(self):
864 """Reads/Downloads the stateful updates and writes it into the device."""
865 if self._image_type == ImageType.FULL:
866 generator_cls = StatefulPayloadGenerator
867 elif self._image_type == ImageType.REMOTE_DIRECTORY:
868 generator_cls = GsFileCopier
869 self._image = os.path.join(self._image,
870 paygen_stateful_payload_lib.STATEFUL_FILE)
871 else:
872 raise ValueError(f'Invalid image type {self._image_type}')
873
874 with generator_cls(self._image) as generator:
875 try:
876 updater = stateful_updater.StatefulUpdater(self._device)
877 updater.Update(
878 generator.Target(),
879 is_payload_on_device=False,
880 update_type=(stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER if
881 self._clobber_stateful else None))
882 finally:
883 generator.CloseTarget()
884
885 def Revert(self):
886 """Reverts the stateful partition update."""
887 logging.info('Reverting the stateful update.')
888 stateful_updater.StatefulUpdater(self._device).Reset()
Amin Hassani55970562021-02-22 20:49:13 -0800889
890
891class ProgressWatcher(threading.Thread):
892 """A class used for watching the progress of rootfs update."""
893
894 def __init__(self, device, target_root: str):
895 """Initializes the class.
896
897 Args:
898 device: The ChromiumOSDevice to be updated.
899 target_root: The target root partition to monitor the progress of.
900 """
901 super().__init__()
902
903 self._device = device
904 self._target_root = target_root
905 self._exit = False
906
907 def __enter__(self):
908 """Starts the thread."""
909 self.start()
910 return self
911
912 def __exit__(self, *args, **kwargs):
913 """Exists the thread."""
914 self._exit = True
915 self.join()
916
917 def _ShouldExit(self):
918 return self._exit
919
920 def run(self):
921 """Monitors the progress of the target root partitions' update.
922
923 This is done by periodically, reading the fd position of the process that is
924 writing into the target partition and reporting it back. Then the position
925 is divided by the size of the block device to report an approximate
926 progress.
927 """
928 cmd = ['blockdev', '--getsize64', self._target_root]
929 output = self._device.run(cmd, capture_output=True).stdout.strip()
930 if output is None:
931 raise Error(f'Cannot get the block device size from {output}.')
932 dev_size = int(output)
933
934 # Using lsof to find out which process is writing to the target rootfs.
935 cmd = f'lsof 2>/dev/null | grep {self._target_root}'
936 while not self._ShouldExit():
937 try:
938 output = self._device.run(cmd, capture_output=True,
939 shell=True).stdout.strip()
940 if output:
941 break
942 except cros_build_lib.RunCommandError:
943 continue
944 finally:
945 time.sleep(1)
946
947 # Now that we know which process is writing to it, we can look the fdinfo of
948 # stdout of that process to get its offset. We're assuming there will be no
949 # seek, which is correct.
950 pid = output.split()[1]
951 cmd = ['cat', f'/proc/{pid}/fdinfo/1']
952 while not self._ShouldExit():
953 try:
954 output = self._device.run(cmd, capture_output=True).stdout.strip()
955 m = re.search(r'^pos:\s*(\d+)$', output, flags=re.M)
956 if m:
957 offset = int(m.group(1))
958 # DeviceImagerOperation will look for this log.
959 logging.info('RootFS progress: %f', offset/dev_size)
960 except cros_build_lib.RunCommandError:
961 continue
962 finally:
963 time.sleep(1)
964
965
966class DeviceImagerOperation(operation.ProgressBarOperation):
967 """A class to provide a progress bar for DeviceImager operation."""
968
969 def __init__(self):
970 """Initializes the class."""
971 super().__init__()
972
973 self._progress = 0.0
974
975 def ParseOutput(self, output=None):
976 """Override function to parse the output and provide progress.
977
978 Args:
979 output: The stderr or stdout.
980 """
981 output = self._stdout.read()
982 match = re.findall(r'RootFS progress: (\d+(?:\.\d+)?)', output)
983 if match:
984 progress = float(match[0])
985 self._progress = max(self._progress, progress)
986
987 # If postinstall completes, move half of the remaining progress.
988 if re.findall(r'Postinstall completed', output):
989 self._progress += (1.0 - self._progress) / 2
990
991 # While waiting for reboot, each time, move half of the remaining progress.
992 if re.findall(r'Unable to get new boot_id', output):
993 self._progress += (1.0 - self._progress) / 2
994
995 if re.findall(r'DeviceImager completed.', output):
996 self._progress = 1.0
997
998 self.ProgressBar(self._progress)