blob: a1eceb0a9bcf515ee668017c674c8b60f8553dab [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,
82 clear_tpm_owner: bool = False):
83 """Initialize DeviceImager for flashing a Chromium OS device.
84
85 Args:
86 device: The ChromiumOSDevice to be updated.
87 image: The target image path (can be xBuddy path).
Amin Hassanicf8f0042021-03-12 10:42:13 -080088 board: Board to use.
89 version: Image version to use.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080090 no_rootfs_update: Whether to do rootfs partition update.
91 no_stateful_update: Whether to do stateful partition update.
Jae Hoon Kimcc723e02021-08-16 21:03:21 +000092 no_minios_update: Whether to do minios partition update.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080093 no_reboot: Whether to reboot device after update. The default is True.
94 disable_verification: Whether to disabling rootfs verification on the
95 device.
96 clobber_stateful: Whether to do a clean stateful partition.
97 clear_tpm_owner: If true, it will clear the TPM owner on reboot.
98 """
99
100 self._device = device
101 self._image = image
Amin Hassanicf8f0042021-03-12 10:42:13 -0800102 self._board = board
103 self._version = version
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800104 self._no_rootfs_update = no_rootfs_update
105 self._no_stateful_update = no_stateful_update
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000106 self._no_minios_update = no_minios_update
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800107 self._no_reboot = no_reboot
108 self._disable_verification = disable_verification
109 self._clobber_stateful = clobber_stateful
110 self._clear_tpm_owner = clear_tpm_owner
111
Amin Hassanib1993eb2021-04-28 12:00:11 -0700112 self._image_type = None
Amin Hassani0972d392021-03-31 19:04:19 -0700113 self._compression = cros_build_lib.COMP_GZIP
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800114 self._inactive_state = None
115
116 def Run(self):
117 """Update the device with image of specific version."""
Amin Hassanib1993eb2021-04-28 12:00:11 -0700118 self._LocateImage()
119 logging.notice('Preparing to update the remote device %s with image %s',
120 self._device.hostname, self._image)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800121
122 try:
Amin Hassani55970562021-02-22 20:49:13 -0800123 if command.UseProgressBar():
124 op = DeviceImagerOperation()
125 op.Run(self._Run)
126 else:
127 self._Run()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800128 except Exception as e:
129 raise Error(f'DeviceImager Failed with error: {e}')
130
Amin Hassani55970562021-02-22 20:49:13 -0800131 # DeviceImagerOperation will look for this log.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800132 logging.info('DeviceImager completed.')
133
134 def _Run(self):
135 """Runs the various operations to install the image on device."""
Amin Hassanib1993eb2021-04-28 12:00:11 -0700136 # Override the compression as remote quick provision images are gzip
137 # compressed only.
138 if self._image_type == ImageType.REMOTE_DIRECTORY:
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800139 self._compression = cros_build_lib.COMP_GZIP
140
Amin Hassanib1993eb2021-04-28 12:00:11 -0700141 self._InstallPartitions()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800142
143 if self._clear_tpm_owner:
144 self._device.ClearTpmOwner()
145
146 if not self._no_reboot:
147 self._Reboot()
148 self._VerifyBootExpectations()
149
150 if self._disable_verification:
151 self._device.DisableRootfsVerification()
152
Amin Hassanib1993eb2021-04-28 12:00:11 -0700153 def _LocateImage(self):
154 """Locates the path to the final image(s) that need to be installed.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800155
156 If the paths is local, the image should be the Chromium OS GPT image
157 (e.g. chromiumos_test_image.bin). If the path is remote, it should be the
158 remote directory where we can find the quick-provision and stateful update
159 files (e.g. gs://chromeos-image-archive/eve-release/R90-x.x.x).
160
161 NOTE: At this point there is no caching involved. Hence we always download
162 the partition payloads or extract them from the Chromium OS image.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800163 """
164 if os.path.isfile(self._image):
Amin Hassanib1993eb2021-04-28 12:00:11 -0700165 self._image_type = ImageType.FULL
166 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800167
168 # TODO(b/172212406): We could potentially also allow this by searching
169 # through the directory to see whether we have quick-provision and stateful
170 # payloads. This only makes sense when a user has their workstation at home
171 # and doesn't want to incur the bandwidth cost of downloading the same
172 # image multiple times. For that, they can simply download the GPT image
173 # image first and flash that instead.
174 if os.path.isdir(self._image):
175 raise ValueError(
176 f'{self._image}: input must be a disk image, not a directory.')
177
178 if gs.PathIsGs(self._image):
179 # TODO(b/172212406): Check whether it is a directory. If it wasn't a
180 # directory download the image into some temp location and use it instead.
Amin Hassanib1993eb2021-04-28 12:00:11 -0700181 self._image_type = ImageType.REMOTE_DIRECTORY
182 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800183
184 # Assuming it is an xBuddy path.
Amin Hassanicf8f0042021-03-12 10:42:13 -0800185 board = cros_build_lib.GetBoard(
186 device_board=self._device.board or flash.GetDefaultBoard(),
187 override_board=self._board, force=True)
188
Amin Hassania20d20d2021-04-28 10:18:18 -0700189 xb = xbuddy.XBuddy(board=board, version=self._version)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800190 build_id, local_file = xb.Translate([self._image])
191 if build_id is None:
192 raise Error(f'{self._image}: unable to find matching xBuddy path.')
193 logging.info('XBuddy path translated to build ID %s', build_id)
194
195 if local_file:
Amin Hassanib1993eb2021-04-28 12:00:11 -0700196 self._image = local_file
197 self._image_type = ImageType.FULL
198 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800199
Amin Hassanib1993eb2021-04-28 12:00:11 -0700200 self._image = f'{devserver_constants.GS_IMAGE_DIR}/{build_id}'
201 self._image_type = ImageType.REMOTE_DIRECTORY
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800202
203 def _SplitDevPath(self, path: str) -> Tuple[str, int]:
204 """Splits the given /dev/x path into prefix and the dev number.
205
206 Args:
207 path: The path to a block dev device.
208
209 Returns:
210 A tuple of representing the prefix and the index of the dev path.
211 e.g.: '/dev/mmcblk0p1' -> ['/dev/mmcblk0p', 1]
212 """
213 match = re.search(r'(.*)([0-9]+)$', path)
214 if match is None:
215 raise Error(f'{path}: Could not parse root dev path.')
216
217 return match.group(1), int(match.group(2))
218
219 def _GetKernelState(self, root_num: int) -> Tuple[Dict, Dict]:
220 """Returns the kernel state.
221
222 Returns:
223 A tuple of two dictionaries: The current active kernel state and the
224 inactive kernel state. (Look at A and B constants in this class.)
225 """
226 if root_num == self.A[Partition.ROOTFS]:
227 return self.A, self.B
228 elif root_num == self.B[Partition.ROOTFS]:
229 return self.B, self.A
230 else:
231 raise Error(f'Invalid root partition number {root_num}')
232
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000233 def _GetMiniOSState(self, minios_num: int) -> Tuple[Dict, Dict]:
234 """Returns the miniOS state.
235
236 Returns:
237 A tuple of dictionaries: The current active miniOS state and the inactive
238 miniOS state.
239 """
240 if minios_num == self.MINIOS_A[Partition.MINIOS]:
241 return self.MINIOS_A, self.MINIOS_B
242 elif minios_num == self.MINIOS_B[Partition.MINIOS]:
243 return self.MINIOS_B, self.MINIOS_A
244 else:
245 raise Error(f'Invalid minios partition number {minios_num}')
246
Amin Hassanib1993eb2021-04-28 12:00:11 -0700247 def _InstallPartitions(self):
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800248 """The main method that installs the partitions of a Chrome OS device.
249
250 It uses parallelism to install the partitions as fast as possible.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800251 """
Amin Hassanid684e982021-02-26 11:10:58 -0800252 prefix, root_num = self._SplitDevPath(self._device.root_dev)
253 active_state, self._inactive_state = self._GetKernelState(root_num)
254
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800255 updaters = []
Amin Hassanid684e982021-02-26 11:10:58 -0800256 if not self._no_rootfs_update:
Amin Hassani75c5f942021-02-20 23:56:53 -0800257 current_root = prefix + str(active_state[Partition.ROOTFS])
258 target_root = prefix + str(self._inactive_state[Partition.ROOTFS])
Amin Hassanib1993eb2021-04-28 12:00:11 -0700259 updaters.append(RootfsUpdater(current_root, self._device, self._image,
260 self._image_type, target_root,
261 self._compression))
Amin Hassani75c5f942021-02-20 23:56:53 -0800262
Amin Hassanid684e982021-02-26 11:10:58 -0800263 target_kernel = prefix + str(self._inactive_state[Partition.KERNEL])
Amin Hassanib1993eb2021-04-28 12:00:11 -0700264 updaters.append(KernelUpdater(self._device, self._image, self._image_type,
Amin Hassanid684e982021-02-26 11:10:58 -0800265 target_kernel, self._compression))
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800266
Amin Hassani74403082021-02-22 11:40:09 -0800267 if not self._no_stateful_update:
268 updaters.append(StatefulUpdater(self._clobber_stateful, self._device,
Amin Hassanib1993eb2021-04-28 12:00:11 -0700269 self._image, self._image_type, None,
270 None))
Amin Hassani74403082021-02-22 11:40:09 -0800271
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000272 if not self._no_minios_update:
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700273 minios_priority = self._device.run(
274 ['crossystem', constants.MINIOS_PRIORITY]).stdout
275 if minios_priority not in ['A', 'B']:
276 logging.warning('Skipping miniOS flash due to missing priority.')
277 else:
278 # Reference disk_layout_v3 for partition numbering.
279 _, inactive_minios_state = self._GetMiniOSState(
280 9 if minios_priority == 'A' else 10)
281 target_minios = prefix + str(inactive_minios_state[Partition.MINIOS])
282 minios_updater = MiniOSUpdater(self._device, self._image,
283 self._image_type, target_minios,
284 self._compression)
285 updaters.append(minios_updater)
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000286
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800287 # Retry the partitions updates that failed, in case a transient error (like
288 # SSH drop, etc) caused the error.
289 num_retries = 1
290 try:
291 retry_util.RetryException(Error, num_retries,
292 parallel.RunParallelSteps,
293 (x.Run for x in updaters if not x.IsFinished()),
294 halt_on_error=True)
295 except Exception:
296 # If one of the partitions failed to be installed, revert all partitions.
297 parallel.RunParallelSteps(x.Revert for x in updaters)
298 raise
299
300 def _Reboot(self):
301 """Reboots the device."""
302 try:
Amin Hassani9281b682021-03-08 16:38:25 -0800303 self._device.Reboot(timeout_sec=300)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800304 except remote_access.RebootError:
305 raise Error('Could not recover from reboot. Once example reason'
306 ' could be the image provided was a non-test image'
307 ' or the system failed to boot after the update.')
308 except Exception as e:
309 raise Error(f'Failed to reboot to the device with error: {e}')
310
311 def _VerifyBootExpectations(self):
312 """Verify that we fully booted into the expected kernel state."""
313 # Discover the newly active kernel.
314 _, root_num = self._SplitDevPath(self._device.root_dev)
315 active_state, _ = self._GetKernelState(root_num)
316
317 # If this happens, we should rollback.
318 if active_state != self._inactive_state:
319 raise Error('The expected kernel state after update is invalid.')
320
321 logging.info('Verified boot expectations.')
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800322
323
324class ReaderBase(threading.Thread):
325 """The base class for reading different inputs and writing into output.
326
327 This class extends threading.Thread, so it will be run on its own thread. Also
328 it can be used as a context manager. Internally, it opens necessary files for
329 writing to and reading from. This class cannot be instantiated, it needs to be
330 sub-classed first to provide necessary function implementations.
331 """
332
333 def __init__(self, use_named_pipes: bool = False):
334 """Initializes the class.
335
336 Args:
337 use_named_pipes: Whether to use a named pipe or anonymous file
338 descriptors.
339 """
340 super().__init__()
341 self._use_named_pipes = use_named_pipes
342 self._pipe_target = None
343 self._pipe_source = None
344
345 def __del__(self):
346 """Destructor.
347
348 Make sure to clean up any named pipes we might have created.
349 """
350 if self._use_named_pipes:
351 osutils.SafeUnlink(self._pipe_target)
352
353 def __enter__(self):
354 """Enters the context manager"""
355 if self._use_named_pipes:
356 # There is no need for the temp file, we only need its path. So the named
357 # pipe is created after this temp file is deleted.
358 with tempfile.NamedTemporaryFile(prefix='chromite-device-imager') as fp:
359 self._pipe_target = self._pipe_source = fp.name
360 os.mkfifo(self._pipe_target)
361 else:
362 self._pipe_target, self._pipe_source = os.pipe()
363
364 self.start()
365 return self
366
367 def __exit__(self, *args, **kwargs):
368 """Exits the context manager."""
369 self.join()
370
371 def _Source(self):
372 """Returns the source pipe to write data into.
373
374 Sub-classes can use this function to determine where to write their data
375 into.
376 """
377 return self._pipe_source
378
379 def _CloseSource(self):
380 """Closes the source pipe.
381
382 Sub-classes should use this function to close the pipe after they are done
383 writing into it. Failure to do so may result reader of the data to hang
384 indefinitely.
385 """
386 if not self._use_named_pipes:
387 os.close(self._pipe_source)
388
389 def Target(self):
390 """Returns the target pipe to read data from.
391
392 Users of this class can use this path to read data from.
393 """
394 return self._pipe_target
395
396 def CloseTarget(self):
397 """Closes the target pipe.
398
399 Users of this class should use this function to close the pipe after they
400 are done reading from it.
401 """
402 if self._use_named_pipes:
403 os.remove(self._pipe_target)
404 else:
405 os.close(self._pipe_target)
406
407
408class PartialFileReader(ReaderBase):
409 """A class to read specific offset and length from a file and compress it.
410
411 This class can be used to read from specific location and length in a file
412 (e.g. A partition in a GPT image). Then it compresses the input and writes it
413 out (to a pipe). Look at the base class for more information.
414 """
415
416 # The offset of different partitions in a Chromium OS image does not always
417 # align to larger values like 4096. It seems that 512 is the maximum value to
418 # be divisible by partition offsets. This size should not be increased just
419 # for 'performance reasons'. Since we are doing everything in parallel, in
420 # practice there is not much difference between this and larger block sizes as
421 # parallelism hides the possible extra latency provided by smaller block
422 # sizes.
423 _BLOCK_SIZE = 512
424
425 def __init__(self, image: str, offset: int, length: int, compression):
426 """Initializes the class.
427
428 Args:
429 image: The path to an image (local or remote directory).
430 offset: The offset (in bytes) to read from the image.
431 length: The length (in bytes) to read from the image.
432 compression: The compression type (see cros_build_lib.COMP_XXX).
433 """
434 super().__init__()
435
436 self._image = image
437 self._offset = offset
438 self._length = length
439 self._compression = compression
440
441 def run(self):
442 """Runs the reading and compression."""
443 cmd = [
444 'dd',
445 'status=none',
446 f'if={self._image}',
447 f'ibs={self._BLOCK_SIZE}',
448 f'skip={int(self._offset/self._BLOCK_SIZE)}',
449 f'count={int(self._length/self._BLOCK_SIZE)}',
450 '|',
451 cros_build_lib.FindCompressor(self._compression),
452 ]
453
454 try:
455 cros_build_lib.run(' '.join(cmd), stdout=self._Source(), shell=True)
456 finally:
457 self._CloseSource()
458
459
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800460class GsFileCopier(ReaderBase):
461 """A class for downloading gzip compressed file from GS bucket into a pipe."""
462
463 def __init__(self, image: str):
464 """Initializes the class.
465
466 Args:
467 image: The path to an image (local or remote directory).
468 """
469 super().__init__(use_named_pipes=True)
470 self._image = image
471
472 def run(self):
473 """Runs the download and write into the output pipe."""
474 try:
475 gs.GSContext().Copy(self._image, self._Source())
476 finally:
477 self._CloseSource()
478
479
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800480class PartitionUpdaterBase(object):
481 """A base abstract class to use for installing an image into a partition.
482
483 Sub-classes should implement the abstract methods to provide the core
484 functionality.
485 """
486 def __init__(self, device, image: str, image_type, target: str, compression):
487 """Initializes this base class with values that most sub-classes will need.
488
489 Args:
490 device: The ChromiumOSDevice to be updated.
491 image: The target image path for the partition update.
492 image_type: The type of the image (ImageType).
493 target: The target path (e.g. block dev) to install the update.
494 compression: The compression used for compressing the update payload.
495 """
496 self._device = device
497 self._image = image
498 self._image_type = image_type
499 self._target = target
500 self._compression = compression
501 self._finished = False
502
503 def Run(self):
504 """The main function that does the partition update job."""
Alex Klein18ef1212021-10-14 12:49:02 -0600505 with timer.Timer() as t:
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800506 try:
507 self._Run()
508 finally:
509 self._finished = True
510
Alex Klein18ef1212021-10-14 12:49:02 -0600511 logging.debug('Completed %s in %s', self.__class__.__name__, t)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800512
513 @abc.abstractmethod
514 def _Run(self):
515 """The method that need to be implemented by sub-classes."""
516 raise NotImplementedError('Sub-classes need to implement this.')
517
518 def IsFinished(self):
519 """Returns whether the partition update has been successful."""
520 return self._finished
521
522 @abc.abstractmethod
523 def Revert(self):
524 """Reverts the partition update.
525
526 Sub-classes need to implement this function to provide revert capability.
527 """
528 raise NotImplementedError('Sub-classes need to implement this.')
529
530
531class RawPartitionUpdater(PartitionUpdaterBase):
532 """A class to update a raw partition on a Chromium OS device."""
533
534 def _Run(self):
535 """The function that does the job of kernel partition update."""
536 if self._image_type == ImageType.FULL:
537 self._CopyPartitionFromImage(self._GetPartitionName())
538 elif self._image_type == ImageType.REMOTE_DIRECTORY:
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800539 self._RedirectPartition(self._GetRemotePartitionName())
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800540 else:
541 raise ValueError(f'Invalid image type {self._image_type}')
542
543 def _GetPartitionName(self):
544 """Returns the name of the partition in a Chromium OS GPT layout.
545
546 Subclasses should override this function to return correct name.
547 """
548 raise NotImplementedError('Subclasses need to implement this.')
549
550 def _CopyPartitionFromImage(self, part_name: str):
551 """Updates the device's partition from a local Chromium OS image.
552
553 Args:
554 part_name: The name of the partition in the source image that needs to be
555 extracted.
556 """
557 cmd = self._GetWriteToTargetCommand()
558
559 offset, length = self._GetPartLocation(part_name)
560 offset, length = self._OptimizePartLocation(offset, length)
561 with PartialFileReader(self._image, offset, length,
562 self._compression) as generator:
563 try:
564 self._device.run(cmd, input=generator.Target(), shell=True)
565 finally:
566 generator.CloseTarget()
567
568 def _GetWriteToTargetCommand(self):
569 """Returns a write to target command to run on a Chromium OS device.
570
571 Returns:
572 A string command to run on a device to read data from stdin, uncompress it
573 and write it to the target partition.
574 """
575 cmd = self._device.GetDecompressor(self._compression)
576 # Using oflag=direct to tell the OS not to cache the writes (faster).
577 cmd += ['|', 'dd', 'bs=1M', 'oflag=direct', f'of={self._target}']
578 return ' '.join(cmd)
579
580 def _GetPartLocation(self, part_name: str):
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000581 """Extracts the location and size of the raw partition from the image.
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800582
583 Args:
584 part_name: The name of the partition in the source image that needs to be
585 extracted.
586
587 Returns:
588 A tuple of offset and length (in bytes) from the image.
589 """
590 try:
591 parts = image_lib.GetImageDiskPartitionInfo(self._image)
592 part_info = [p for p in parts if p.name == part_name][0]
593 except IndexError:
594 raise Error(f'No partition named {part_name} found.')
595
596 return int(part_info.start), int(part_info.size)
597
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800598 def _GetRemotePartitionName(self):
599 """Returns the name of the quick-provision partition file.
600
601 Subclasses should override this function to return correct name.
602 """
603 raise NotImplementedError('Subclasses need to implement this.')
604
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800605 def _OptimizePartLocation(self, offset: int, length: int):
606 """Optimizes the offset and length of the partition.
607
608 Subclasses can override this to provide better offset/length than what is
609 defined in the PGT partition layout.
610
611 Args:
612 offset: The offset (in bytes) of the partition in the image.
613 length: The length (in bytes) of the partition.
614
615 Returns:
616 A tuple of offset and length (in bytes) from the image.
617 """
618 return offset, length
Amin Hassanid684e982021-02-26 11:10:58 -0800619
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800620 def _RedirectPartition(self, file_name: str):
621 """Downloads the partition from a remote path and writes it into target.
622
623 Args:
624 file_name: The file name in the remote directory self._image.
625 """
626 cmd = self._GetWriteToTargetCommand()
627
628 image_path = os.path.join(self._image, file_name)
629 with GsFileCopier(image_path) as generator:
630 try:
631 with open(generator.Target(), 'rb') as fp:
632 self._device.run(cmd, input=fp, shell=True)
633 finally:
634 generator.CloseTarget()
635
Amin Hassanid684e982021-02-26 11:10:58 -0800636
637class KernelUpdater(RawPartitionUpdater):
638 """A class to update the kernel partition on a Chromium OS device."""
639
640 def _GetPartitionName(self):
641 """See RawPartitionUpdater._GetPartitionName()."""
642 return constants.PART_KERN_B
643
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800644 def _GetRemotePartitionName(self):
645 """See RawPartitionUpdater._GetRemotePartitionName()."""
646 return constants.QUICK_PROVISION_PAYLOAD_KERNEL
647
Amin Hassanid684e982021-02-26 11:10:58 -0800648 def Revert(self):
649 """Reverts the kernel partition update."""
650 # There is nothing to do for reverting kernel partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800651
652
653class RootfsUpdater(RawPartitionUpdater):
654 """A class to update the root partition on a Chromium OS device."""
655
656 def __init__(self, current_root: str, *args):
657 """Initializes the class.
658
659 Args:
660 current_root: The current root device path.
661 *args: See PartitionUpdaterBase
662 """
663 super().__init__(*args)
664
665 self._current_root = current_root
666 self._ran_postinst = False
667
668 def _GetPartitionName(self):
669 """See RawPartitionUpdater._GetPartitionName()."""
670 return constants.PART_ROOT_A
671
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800672 def _GetRemotePartitionName(self):
673 """See RawPartitionUpdater._GetRemotePartitionName()."""
674 return constants.QUICK_PROVISION_PAYLOAD_ROOTFS
675
Amin Hassani75c5f942021-02-20 23:56:53 -0800676 def _Run(self):
677 """The function that does the job of rootfs partition update."""
Amin Hassani55970562021-02-22 20:49:13 -0800678 with ProgressWatcher(self._device, self._target):
679 super()._Run()
Amin Hassani75c5f942021-02-20 23:56:53 -0800680
681 self._RunPostInst()
682
683 def _OptimizePartLocation(self, offset: int, length: int):
684 """Optimizes the size of the root partition of the image.
685
686 Normally the file system does not occupy the entire partition. Furthermore
687 we don't need the verity hash tree at the end of the root file system
688 because postinst will recreate it. This function reads the (approximate)
689 superblock of the ext4 partition and extracts the actual file system size in
690 the root partition.
691 """
692 superblock_size = 4096 * 2
693 with open(self._image, 'rb') as r:
694 r.seek(offset)
695 with tempfile.NamedTemporaryFile(delete=False) as fp:
696 fp.write(r.read(superblock_size))
697 fp.close()
698 return offset, partition_lib.Ext2FileSystemSize(fp.name)
699
700 def _RunPostInst(self, on_target: bool = True):
701 """Runs the postinst process in the root partition.
702
703 Args:
704 on_target: If true the postinst is run on the target (inactive)
705 partition. This is used when doing normal updates. If false, the
706 postinst is run on the current (active) partition. This is used when
707 reverting an update.
708 """
709 try:
710 postinst_dir = '/'
711 partition = self._current_root
712 if on_target:
713 postinst_dir = self._device.run(
714 ['mktemp', '-d', '-p', self._device.work_dir],
715 capture_output=True).stdout.strip()
716 self._device.run(['mount', '-o', 'ro', self._target, postinst_dir])
717 partition = self._target
718
719 self._ran_postinst = True
720 postinst = os.path.join(postinst_dir, 'postinst')
721 result = self._device.run([postinst, partition], capture_output=True)
722
723 logging.debug('Postinst result on %s: \n%s', postinst, result.stdout)
Amin Hassani55970562021-02-22 20:49:13 -0800724 # DeviceImagerOperation will look for this log.
Amin Hassani75c5f942021-02-20 23:56:53 -0800725 logging.info('Postinstall completed.')
726 finally:
727 if on_target:
728 self._device.run(['umount', postinst_dir])
729
730 def Revert(self):
731 """Reverts the root update install."""
732 logging.info('Reverting the rootfs partition update.')
733 if self._ran_postinst:
734 # We don't have to do anything for revert if we haven't changed the kernel
735 # priorities yet.
736 self._RunPostInst(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800737
738
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000739class MiniOSUpdater(RawPartitionUpdater):
740 """A class to update the miniOS partition on a Chromium OS device."""
741
742 def __init__(self, *args):
743 """Initializes the class.
744
745 Args:
746 *args: See PartitionUpdaterBase
747 """
748 super().__init__(*args)
749
750 self._ran_postinst = False
751
752 def _GetPartitionName(self):
753 """See RawPartitionUpdater._GetPartitionName()."""
754 return constants.PART_MINIOS_A
755
756 def _GetRemotePartitionName(self):
757 """See RawPartitionUpdater._GetRemotePartitionName()."""
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700758 return constants.QUICK_PROVISION_PAYLOAD_MINIOS
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000759
760 def _Run(self):
761 """The function that does the job of rootfs partition update."""
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700762 if self._image_type == ImageType.FULL:
763 if self._MiniOSPartitionsExistInImage():
764 logging.info('Updating miniOS partition from local.')
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000765 super()._Run()
766 else:
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700767 logging.warning('Not updating miniOS partition as it does not exist.')
768 return
769 elif self._image_type == ImageType.REMOTE_DIRECTORY:
770 if not gs.GSContext().Exists(
771 os.path.join(self._image,
772 constants.QUICK_PROVISION_PAYLOAD_MINIOS)):
773 logging.warning('Not updating miniOS, missing remote files.')
774 return
775 elif not self._MiniOSPartitionsExist():
776 logging.warning('Not updating miniOS, missing partitions.')
777 return
778 else:
779 logging.info('Updating miniOS partition from remote.')
780 super()._Run()
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000781 else:
782 # Let super() handle this error.
783 super()._Run()
784
785 self._RunPostInstall()
786
787 def _RunPostInstall(self):
788 """The function will change the priority of the miniOS partitions."""
789 self._FlipMiniOSPriority()
790 self._ran_postinst = True
791
792 def Revert(self):
793 """Reverts the miniOS partition update."""
794 if self._ran_postinst:
795 self._FlipMiniOSPriority()
796
797 def _GetMiniOSPriority(self):
798 return self._device.run(['crossystem', constants.MINIOS_PRIORITY]).output
799
800 def _SetMiniOSPriority(self, priority: str):
801 self._device.run(
802 ['crossystem', f'{constants.MINIOS_PRIORITY}={priority}'])
803
804 def _FlipMiniOSPriority(self):
805 inactive_minios_priority = 'B' if self._GetMiniOSPriority() == 'A' else 'A'
806 logging.info('Setting miniOS priority to %s', inactive_minios_priority)
807 self._SetMiniOSPriority(inactive_minios_priority)
808
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700809 def _MiniOSPartitionsExistInImage(self):
810 """Checks if miniOS partition exists in the image."""
811 d = cgpt.Disk.FromImage(self._image)
812 try:
813 d.GetPartitionByTypeGuid(cgpt.MINIOS_TYPE_GUID)
814 return True
815 except KeyError:
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000816 return False
817
Jae Hoon Kimb88b7962021-10-18 11:08:38 -0700818 def _MiniOSPartitionsExist(self):
819 """Checks if the device has miniOS partitions."""
820 run = lambda x: self._device.run(x).stdout.strip()
821 device_drive = run(['rootdev', '-s', '-d'])
822 cmd = ['cgpt', 'show', '-t', device_drive, '-i']
823 return all((run(cmd + [p]) == cgpt.MINIOS_TYPE_GUID) for p in ('9', '10'))
824
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000825
Amin Hassani74403082021-02-22 11:40:09 -0800826class StatefulPayloadGenerator(ReaderBase):
827 """A class for generating a stateful update payload in a separate thread."""
828 def __init__(self, image: str):
829 """Initializes that class.
830
831 Args:
832 image: The path to a local Chromium OS image.
833 """
834 super().__init__()
835 self._image = image
836
837 def run(self):
838 """Generates the stateful update and writes it into the output pipe."""
839 try:
840 paygen_stateful_payload_lib.GenerateStatefulPayload(
841 self._image, self._Source())
842 finally:
843 self._CloseSource()
844
845
846class StatefulUpdater(PartitionUpdaterBase):
847 """A class to update the stateful partition on a device."""
848 def __init__(self, clobber_stateful: bool, *args):
849 """Initializes the class
850
851 Args:
852 clobber_stateful: Whether to clobber the stateful or not.
853 *args: Look at PartitionUpdaterBase.
854 """
855 super().__init__(*args)
856 self._clobber_stateful = clobber_stateful
857
858 def _Run(self):
859 """Reads/Downloads the stateful updates and writes it into the device."""
860 if self._image_type == ImageType.FULL:
861 generator_cls = StatefulPayloadGenerator
862 elif self._image_type == ImageType.REMOTE_DIRECTORY:
863 generator_cls = GsFileCopier
864 self._image = os.path.join(self._image,
865 paygen_stateful_payload_lib.STATEFUL_FILE)
866 else:
867 raise ValueError(f'Invalid image type {self._image_type}')
868
869 with generator_cls(self._image) as generator:
870 try:
871 updater = stateful_updater.StatefulUpdater(self._device)
872 updater.Update(
873 generator.Target(),
874 is_payload_on_device=False,
875 update_type=(stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER if
876 self._clobber_stateful else None))
877 finally:
878 generator.CloseTarget()
879
880 def Revert(self):
881 """Reverts the stateful partition update."""
882 logging.info('Reverting the stateful update.')
883 stateful_updater.StatefulUpdater(self._device).Reset()
Amin Hassani55970562021-02-22 20:49:13 -0800884
885
886class ProgressWatcher(threading.Thread):
887 """A class used for watching the progress of rootfs update."""
888
889 def __init__(self, device, target_root: str):
890 """Initializes the class.
891
892 Args:
893 device: The ChromiumOSDevice to be updated.
894 target_root: The target root partition to monitor the progress of.
895 """
896 super().__init__()
897
898 self._device = device
899 self._target_root = target_root
900 self._exit = False
901
902 def __enter__(self):
903 """Starts the thread."""
904 self.start()
905 return self
906
907 def __exit__(self, *args, **kwargs):
908 """Exists the thread."""
909 self._exit = True
910 self.join()
911
912 def _ShouldExit(self):
913 return self._exit
914
915 def run(self):
916 """Monitors the progress of the target root partitions' update.
917
918 This is done by periodically, reading the fd position of the process that is
919 writing into the target partition and reporting it back. Then the position
920 is divided by the size of the block device to report an approximate
921 progress.
922 """
923 cmd = ['blockdev', '--getsize64', self._target_root]
924 output = self._device.run(cmd, capture_output=True).stdout.strip()
925 if output is None:
926 raise Error(f'Cannot get the block device size from {output}.')
927 dev_size = int(output)
928
929 # Using lsof to find out which process is writing to the target rootfs.
930 cmd = f'lsof 2>/dev/null | grep {self._target_root}'
931 while not self._ShouldExit():
932 try:
933 output = self._device.run(cmd, capture_output=True,
934 shell=True).stdout.strip()
935 if output:
936 break
937 except cros_build_lib.RunCommandError:
938 continue
939 finally:
940 time.sleep(1)
941
942 # Now that we know which process is writing to it, we can look the fdinfo of
943 # stdout of that process to get its offset. We're assuming there will be no
944 # seek, which is correct.
945 pid = output.split()[1]
946 cmd = ['cat', f'/proc/{pid}/fdinfo/1']
947 while not self._ShouldExit():
948 try:
949 output = self._device.run(cmd, capture_output=True).stdout.strip()
950 m = re.search(r'^pos:\s*(\d+)$', output, flags=re.M)
951 if m:
952 offset = int(m.group(1))
953 # DeviceImagerOperation will look for this log.
954 logging.info('RootFS progress: %f', offset/dev_size)
955 except cros_build_lib.RunCommandError:
956 continue
957 finally:
958 time.sleep(1)
959
960
961class DeviceImagerOperation(operation.ProgressBarOperation):
962 """A class to provide a progress bar for DeviceImager operation."""
963
964 def __init__(self):
965 """Initializes the class."""
966 super().__init__()
967
968 self._progress = 0.0
969
970 def ParseOutput(self, output=None):
971 """Override function to parse the output and provide progress.
972
973 Args:
974 output: The stderr or stdout.
975 """
976 output = self._stdout.read()
977 match = re.findall(r'RootFS progress: (\d+(?:\.\d+)?)', output)
978 if match:
979 progress = float(match[0])
980 self._progress = max(self._progress, progress)
981
982 # If postinstall completes, move half of the remaining progress.
983 if re.findall(r'Postinstall completed', output):
984 self._progress += (1.0 - self._progress) / 2
985
986 # While waiting for reboot, each time, move half of the remaining progress.
987 if re.findall(r'Unable to get new boot_id', output):
988 self._progress += (1.0 - self._progress) / 2
989
990 if re.findall(r'DeviceImager completed.', output):
991 self._progress = 1.0
992
993 self.ProgressBar(self._progress)