blob: 1b32a01bf6d2ef3476058609e8704ae970f5ef3f [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
Amin Hassanid4b3ff82021-02-20 23:05:14 -080019from chromite.lib import constants
Amin Hassani92f6c4a2021-02-20 17:36:09 -080020from chromite.lib import cros_build_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080021from chromite.lib import gs
Amin Hassanid4b3ff82021-02-20 23:05:14 -080022from chromite.lib import image_lib
Amin Hassani55970562021-02-22 20:49:13 -080023from chromite.lib import operation
Amin Hassanid4b3ff82021-02-20 23:05:14 -080024from chromite.lib import osutils
Amin Hassani92f6c4a2021-02-20 17:36:09 -080025from chromite.lib import parallel
26from chromite.lib import remote_access
27from chromite.lib import retry_util
Amin Hassani74403082021-02-22 11:40:09 -080028from chromite.lib import stateful_updater
Amin Hassani75c5f942021-02-20 23:56:53 -080029from chromite.lib.paygen import partition_lib
Amin Hassani74403082021-02-22 11:40:09 -080030from chromite.lib.paygen import paygen_stateful_payload_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080031from chromite.lib.xbuddy import devserver_constants
32from chromite.lib.xbuddy import xbuddy
33
34
Amin Hassani92f6c4a2021-02-20 17:36:09 -080035class Error(Exception):
36 """Thrown when there is a general Chromium OS-specific flash error."""
37
38
39class ImageType(enum.Enum):
40 """Type of the image that is used for flashing the device."""
41
42 # The full image on disk (e.g. chromiumos_test_image.bin).
43 FULL = 0
44 # The remote directory path
45 # (e.g gs://chromeos-image-archive/eve-release/R90-x.x.x)
46 REMOTE_DIRECTORY = 1
47
48
49class Partition(enum.Enum):
50 """An enum for partition types like kernel and rootfs."""
51 KERNEL = 0
52 ROOTFS = 1
53
54
55class DeviceImager(object):
56 """A class to flash a Chromium OS device.
57
58 This utility uses parallelism as much as possible to achieve its goal as fast
59 as possible. For example, it uses parallel compressors, parallel transfers,
60 and simultaneous pipes.
61 """
62
63 # The parameters of the kernel and rootfs's two main partitions.
64 A = {Partition.KERNEL: 2, Partition.ROOTFS: 3}
65 B = {Partition.KERNEL: 4, Partition.ROOTFS: 5}
66
67 def __init__(self, device, image: str,
Amin Hassanicf8f0042021-03-12 10:42:13 -080068 board: str = None,
69 version: str = None,
Amin Hassani92f6c4a2021-02-20 17:36:09 -080070 no_rootfs_update: bool = False,
71 no_stateful_update: bool = False,
72 no_reboot: bool = False,
73 disable_verification: bool = False,
74 clobber_stateful: bool = False,
75 clear_tpm_owner: bool = False):
76 """Initialize DeviceImager for flashing a Chromium OS device.
77
78 Args:
79 device: The ChromiumOSDevice to be updated.
80 image: The target image path (can be xBuddy path).
Amin Hassanicf8f0042021-03-12 10:42:13 -080081 board: Board to use.
82 version: Image version to use.
Amin Hassani92f6c4a2021-02-20 17:36:09 -080083 no_rootfs_update: Whether to do rootfs partition update.
84 no_stateful_update: Whether to do stateful partition update.
85 no_reboot: Whether to reboot device after update. The default is True.
86 disable_verification: Whether to disabling rootfs verification on the
87 device.
88 clobber_stateful: Whether to do a clean stateful partition.
89 clear_tpm_owner: If true, it will clear the TPM owner on reboot.
90 """
91
92 self._device = device
93 self._image = image
Amin Hassanicf8f0042021-03-12 10:42:13 -080094 self._board = board
95 self._version = version
Amin Hassani92f6c4a2021-02-20 17:36:09 -080096 self._no_rootfs_update = no_rootfs_update
97 self._no_stateful_update = no_stateful_update
98 self._no_reboot = no_reboot
99 self._disable_verification = disable_verification
100 self._clobber_stateful = clobber_stateful
101 self._clear_tpm_owner = clear_tpm_owner
102
Amin Hassanib1993eb2021-04-28 12:00:11 -0700103 self._image_type = None
Amin Hassani0972d392021-03-31 19:04:19 -0700104 self._compression = cros_build_lib.COMP_GZIP
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800105 self._inactive_state = None
106
107 def Run(self):
108 """Update the device with image of specific version."""
Amin Hassanib1993eb2021-04-28 12:00:11 -0700109 self._LocateImage()
110 logging.notice('Preparing to update the remote device %s with image %s',
111 self._device.hostname, self._image)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800112
113 try:
Amin Hassani55970562021-02-22 20:49:13 -0800114 if command.UseProgressBar():
115 op = DeviceImagerOperation()
116 op.Run(self._Run)
117 else:
118 self._Run()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800119 except Exception as e:
120 raise Error(f'DeviceImager Failed with error: {e}')
121
Amin Hassani55970562021-02-22 20:49:13 -0800122 # DeviceImagerOperation will look for this log.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800123 logging.info('DeviceImager completed.')
124
125 def _Run(self):
126 """Runs the various operations to install the image on device."""
Amin Hassanib1993eb2021-04-28 12:00:11 -0700127 # Override the compression as remote quick provision images are gzip
128 # compressed only.
129 if self._image_type == ImageType.REMOTE_DIRECTORY:
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800130 self._compression = cros_build_lib.COMP_GZIP
131
Amin Hassanib1993eb2021-04-28 12:00:11 -0700132 self._InstallPartitions()
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800133
134 if self._clear_tpm_owner:
135 self._device.ClearTpmOwner()
136
137 if not self._no_reboot:
138 self._Reboot()
139 self._VerifyBootExpectations()
140
141 if self._disable_verification:
142 self._device.DisableRootfsVerification()
143
Amin Hassanib1993eb2021-04-28 12:00:11 -0700144 def _LocateImage(self):
145 """Locates the path to the final image(s) that need to be installed.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800146
147 If the paths is local, the image should be the Chromium OS GPT image
148 (e.g. chromiumos_test_image.bin). If the path is remote, it should be the
149 remote directory where we can find the quick-provision and stateful update
150 files (e.g. gs://chromeos-image-archive/eve-release/R90-x.x.x).
151
152 NOTE: At this point there is no caching involved. Hence we always download
153 the partition payloads or extract them from the Chromium OS image.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800154 """
155 if os.path.isfile(self._image):
Amin Hassanib1993eb2021-04-28 12:00:11 -0700156 self._image_type = ImageType.FULL
157 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800158
159 # TODO(b/172212406): We could potentially also allow this by searching
160 # through the directory to see whether we have quick-provision and stateful
161 # payloads. This only makes sense when a user has their workstation at home
162 # and doesn't want to incur the bandwidth cost of downloading the same
163 # image multiple times. For that, they can simply download the GPT image
164 # image first and flash that instead.
165 if os.path.isdir(self._image):
166 raise ValueError(
167 f'{self._image}: input must be a disk image, not a directory.')
168
169 if gs.PathIsGs(self._image):
170 # TODO(b/172212406): Check whether it is a directory. If it wasn't a
171 # directory download the image into some temp location and use it instead.
Amin Hassanib1993eb2021-04-28 12:00:11 -0700172 self._image_type = ImageType.REMOTE_DIRECTORY
173 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800174
175 # Assuming it is an xBuddy path.
Amin Hassanicf8f0042021-03-12 10:42:13 -0800176 board = cros_build_lib.GetBoard(
177 device_board=self._device.board or flash.GetDefaultBoard(),
178 override_board=self._board, force=True)
179
Amin Hassania20d20d2021-04-28 10:18:18 -0700180 xb = xbuddy.XBuddy(board=board, version=self._version)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800181 build_id, local_file = xb.Translate([self._image])
182 if build_id is None:
183 raise Error(f'{self._image}: unable to find matching xBuddy path.')
184 logging.info('XBuddy path translated to build ID %s', build_id)
185
186 if local_file:
Amin Hassanib1993eb2021-04-28 12:00:11 -0700187 self._image = local_file
188 self._image_type = ImageType.FULL
189 return
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800190
Amin Hassanib1993eb2021-04-28 12:00:11 -0700191 self._image = f'{devserver_constants.GS_IMAGE_DIR}/{build_id}'
192 self._image_type = ImageType.REMOTE_DIRECTORY
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800193
194 def _SplitDevPath(self, path: str) -> Tuple[str, int]:
195 """Splits the given /dev/x path into prefix and the dev number.
196
197 Args:
198 path: The path to a block dev device.
199
200 Returns:
201 A tuple of representing the prefix and the index of the dev path.
202 e.g.: '/dev/mmcblk0p1' -> ['/dev/mmcblk0p', 1]
203 """
204 match = re.search(r'(.*)([0-9]+)$', path)
205 if match is None:
206 raise Error(f'{path}: Could not parse root dev path.')
207
208 return match.group(1), int(match.group(2))
209
210 def _GetKernelState(self, root_num: int) -> Tuple[Dict, Dict]:
211 """Returns the kernel state.
212
213 Returns:
214 A tuple of two dictionaries: The current active kernel state and the
215 inactive kernel state. (Look at A and B constants in this class.)
216 """
217 if root_num == self.A[Partition.ROOTFS]:
218 return self.A, self.B
219 elif root_num == self.B[Partition.ROOTFS]:
220 return self.B, self.A
221 else:
222 raise Error(f'Invalid root partition number {root_num}')
223
Amin Hassanib1993eb2021-04-28 12:00:11 -0700224 def _InstallPartitions(self):
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800225 """The main method that installs the partitions of a Chrome OS device.
226
227 It uses parallelism to install the partitions as fast as possible.
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800228 """
Amin Hassanid684e982021-02-26 11:10:58 -0800229 prefix, root_num = self._SplitDevPath(self._device.root_dev)
230 active_state, self._inactive_state = self._GetKernelState(root_num)
231
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800232 updaters = []
Amin Hassanid684e982021-02-26 11:10:58 -0800233 if not self._no_rootfs_update:
Amin Hassani75c5f942021-02-20 23:56:53 -0800234 current_root = prefix + str(active_state[Partition.ROOTFS])
235 target_root = prefix + str(self._inactive_state[Partition.ROOTFS])
Amin Hassanib1993eb2021-04-28 12:00:11 -0700236 updaters.append(RootfsUpdater(current_root, self._device, self._image,
237 self._image_type, target_root,
238 self._compression))
Amin Hassani75c5f942021-02-20 23:56:53 -0800239
Amin Hassanid684e982021-02-26 11:10:58 -0800240 target_kernel = prefix + str(self._inactive_state[Partition.KERNEL])
Amin Hassanib1993eb2021-04-28 12:00:11 -0700241 updaters.append(KernelUpdater(self._device, self._image, self._image_type,
Amin Hassanid684e982021-02-26 11:10:58 -0800242 target_kernel, self._compression))
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800243
Amin Hassani74403082021-02-22 11:40:09 -0800244 if not self._no_stateful_update:
245 updaters.append(StatefulUpdater(self._clobber_stateful, self._device,
Amin Hassanib1993eb2021-04-28 12:00:11 -0700246 self._image, self._image_type, None,
247 None))
Amin Hassani74403082021-02-22 11:40:09 -0800248
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800249 # Retry the partitions updates that failed, in case a transient error (like
250 # SSH drop, etc) caused the error.
251 num_retries = 1
252 try:
253 retry_util.RetryException(Error, num_retries,
254 parallel.RunParallelSteps,
255 (x.Run for x in updaters if not x.IsFinished()),
256 halt_on_error=True)
257 except Exception:
258 # If one of the partitions failed to be installed, revert all partitions.
259 parallel.RunParallelSteps(x.Revert for x in updaters)
260 raise
261
262 def _Reboot(self):
263 """Reboots the device."""
264 try:
Amin Hassani9281b682021-03-08 16:38:25 -0800265 self._device.Reboot(timeout_sec=300)
Amin Hassani92f6c4a2021-02-20 17:36:09 -0800266 except remote_access.RebootError:
267 raise Error('Could not recover from reboot. Once example reason'
268 ' could be the image provided was a non-test image'
269 ' or the system failed to boot after the update.')
270 except Exception as e:
271 raise Error(f'Failed to reboot to the device with error: {e}')
272
273 def _VerifyBootExpectations(self):
274 """Verify that we fully booted into the expected kernel state."""
275 # Discover the newly active kernel.
276 _, root_num = self._SplitDevPath(self._device.root_dev)
277 active_state, _ = self._GetKernelState(root_num)
278
279 # If this happens, we should rollback.
280 if active_state != self._inactive_state:
281 raise Error('The expected kernel state after update is invalid.')
282
283 logging.info('Verified boot expectations.')
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800284
285
286class ReaderBase(threading.Thread):
287 """The base class for reading different inputs and writing into output.
288
289 This class extends threading.Thread, so it will be run on its own thread. Also
290 it can be used as a context manager. Internally, it opens necessary files for
291 writing to and reading from. This class cannot be instantiated, it needs to be
292 sub-classed first to provide necessary function implementations.
293 """
294
295 def __init__(self, use_named_pipes: bool = False):
296 """Initializes the class.
297
298 Args:
299 use_named_pipes: Whether to use a named pipe or anonymous file
300 descriptors.
301 """
302 super().__init__()
303 self._use_named_pipes = use_named_pipes
304 self._pipe_target = None
305 self._pipe_source = None
306
307 def __del__(self):
308 """Destructor.
309
310 Make sure to clean up any named pipes we might have created.
311 """
312 if self._use_named_pipes:
313 osutils.SafeUnlink(self._pipe_target)
314
315 def __enter__(self):
316 """Enters the context manager"""
317 if self._use_named_pipes:
318 # There is no need for the temp file, we only need its path. So the named
319 # pipe is created after this temp file is deleted.
320 with tempfile.NamedTemporaryFile(prefix='chromite-device-imager') as fp:
321 self._pipe_target = self._pipe_source = fp.name
322 os.mkfifo(self._pipe_target)
323 else:
324 self._pipe_target, self._pipe_source = os.pipe()
325
326 self.start()
327 return self
328
329 def __exit__(self, *args, **kwargs):
330 """Exits the context manager."""
331 self.join()
332
333 def _Source(self):
334 """Returns the source pipe to write data into.
335
336 Sub-classes can use this function to determine where to write their data
337 into.
338 """
339 return self._pipe_source
340
341 def _CloseSource(self):
342 """Closes the source pipe.
343
344 Sub-classes should use this function to close the pipe after they are done
345 writing into it. Failure to do so may result reader of the data to hang
346 indefinitely.
347 """
348 if not self._use_named_pipes:
349 os.close(self._pipe_source)
350
351 def Target(self):
352 """Returns the target pipe to read data from.
353
354 Users of this class can use this path to read data from.
355 """
356 return self._pipe_target
357
358 def CloseTarget(self):
359 """Closes the target pipe.
360
361 Users of this class should use this function to close the pipe after they
362 are done reading from it.
363 """
364 if self._use_named_pipes:
365 os.remove(self._pipe_target)
366 else:
367 os.close(self._pipe_target)
368
369
370class PartialFileReader(ReaderBase):
371 """A class to read specific offset and length from a file and compress it.
372
373 This class can be used to read from specific location and length in a file
374 (e.g. A partition in a GPT image). Then it compresses the input and writes it
375 out (to a pipe). Look at the base class for more information.
376 """
377
378 # The offset of different partitions in a Chromium OS image does not always
379 # align to larger values like 4096. It seems that 512 is the maximum value to
380 # be divisible by partition offsets. This size should not be increased just
381 # for 'performance reasons'. Since we are doing everything in parallel, in
382 # practice there is not much difference between this and larger block sizes as
383 # parallelism hides the possible extra latency provided by smaller block
384 # sizes.
385 _BLOCK_SIZE = 512
386
387 def __init__(self, image: str, offset: int, length: int, compression):
388 """Initializes the class.
389
390 Args:
391 image: The path to an image (local or remote directory).
392 offset: The offset (in bytes) to read from the image.
393 length: The length (in bytes) to read from the image.
394 compression: The compression type (see cros_build_lib.COMP_XXX).
395 """
396 super().__init__()
397
398 self._image = image
399 self._offset = offset
400 self._length = length
401 self._compression = compression
402
403 def run(self):
404 """Runs the reading and compression."""
405 cmd = [
406 'dd',
407 'status=none',
408 f'if={self._image}',
409 f'ibs={self._BLOCK_SIZE}',
410 f'skip={int(self._offset/self._BLOCK_SIZE)}',
411 f'count={int(self._length/self._BLOCK_SIZE)}',
412 '|',
413 cros_build_lib.FindCompressor(self._compression),
414 ]
415
416 try:
417 cros_build_lib.run(' '.join(cmd), stdout=self._Source(), shell=True)
418 finally:
419 self._CloseSource()
420
421
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800422class GsFileCopier(ReaderBase):
423 """A class for downloading gzip compressed file from GS bucket into a pipe."""
424
425 def __init__(self, image: str):
426 """Initializes the class.
427
428 Args:
429 image: The path to an image (local or remote directory).
430 """
431 super().__init__(use_named_pipes=True)
432 self._image = image
433
434 def run(self):
435 """Runs the download and write into the output pipe."""
436 try:
437 gs.GSContext().Copy(self._image, self._Source())
438 finally:
439 self._CloseSource()
440
441
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800442class PartitionUpdaterBase(object):
443 """A base abstract class to use for installing an image into a partition.
444
445 Sub-classes should implement the abstract methods to provide the core
446 functionality.
447 """
448 def __init__(self, device, image: str, image_type, target: str, compression):
449 """Initializes this base class with values that most sub-classes will need.
450
451 Args:
452 device: The ChromiumOSDevice to be updated.
453 image: The target image path for the partition update.
454 image_type: The type of the image (ImageType).
455 target: The target path (e.g. block dev) to install the update.
456 compression: The compression used for compressing the update payload.
457 """
458 self._device = device
459 self._image = image
460 self._image_type = image_type
461 self._target = target
462 self._compression = compression
463 self._finished = False
464
465 def Run(self):
466 """The main function that does the partition update job."""
467 with cros_build_lib.TimedSection() as timer:
468 try:
469 self._Run()
470 finally:
471 self._finished = True
472
473 logging.debug('Completed %s in %s', self.__class__.__name__, timer.delta)
474
475 @abc.abstractmethod
476 def _Run(self):
477 """The method that need to be implemented by sub-classes."""
478 raise NotImplementedError('Sub-classes need to implement this.')
479
480 def IsFinished(self):
481 """Returns whether the partition update has been successful."""
482 return self._finished
483
484 @abc.abstractmethod
485 def Revert(self):
486 """Reverts the partition update.
487
488 Sub-classes need to implement this function to provide revert capability.
489 """
490 raise NotImplementedError('Sub-classes need to implement this.')
491
492
493class RawPartitionUpdater(PartitionUpdaterBase):
494 """A class to update a raw partition on a Chromium OS device."""
495
496 def _Run(self):
497 """The function that does the job of kernel partition update."""
498 if self._image_type == ImageType.FULL:
499 self._CopyPartitionFromImage(self._GetPartitionName())
500 elif self._image_type == ImageType.REMOTE_DIRECTORY:
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800501 self._RedirectPartition(self._GetRemotePartitionName())
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800502 else:
503 raise ValueError(f'Invalid image type {self._image_type}')
504
505 def _GetPartitionName(self):
506 """Returns the name of the partition in a Chromium OS GPT layout.
507
508 Subclasses should override this function to return correct name.
509 """
510 raise NotImplementedError('Subclasses need to implement this.')
511
512 def _CopyPartitionFromImage(self, part_name: str):
513 """Updates the device's partition from a local Chromium OS image.
514
515 Args:
516 part_name: The name of the partition in the source image that needs to be
517 extracted.
518 """
519 cmd = self._GetWriteToTargetCommand()
520
521 offset, length = self._GetPartLocation(part_name)
522 offset, length = self._OptimizePartLocation(offset, length)
523 with PartialFileReader(self._image, offset, length,
524 self._compression) as generator:
525 try:
526 self._device.run(cmd, input=generator.Target(), shell=True)
527 finally:
528 generator.CloseTarget()
529
530 def _GetWriteToTargetCommand(self):
531 """Returns a write to target command to run on a Chromium OS device.
532
533 Returns:
534 A string command to run on a device to read data from stdin, uncompress it
535 and write it to the target partition.
536 """
537 cmd = self._device.GetDecompressor(self._compression)
538 # Using oflag=direct to tell the OS not to cache the writes (faster).
539 cmd += ['|', 'dd', 'bs=1M', 'oflag=direct', f'of={self._target}']
540 return ' '.join(cmd)
541
542 def _GetPartLocation(self, part_name: str):
543 """Extracts the location and size of the kernel partition from the image.
544
545 Args:
546 part_name: The name of the partition in the source image that needs to be
547 extracted.
548
549 Returns:
550 A tuple of offset and length (in bytes) from the image.
551 """
552 try:
553 parts = image_lib.GetImageDiskPartitionInfo(self._image)
554 part_info = [p for p in parts if p.name == part_name][0]
555 except IndexError:
556 raise Error(f'No partition named {part_name} found.')
557
558 return int(part_info.start), int(part_info.size)
559
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800560 def _GetRemotePartitionName(self):
561 """Returns the name of the quick-provision partition file.
562
563 Subclasses should override this function to return correct name.
564 """
565 raise NotImplementedError('Subclasses need to implement this.')
566
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800567 def _OptimizePartLocation(self, offset: int, length: int):
568 """Optimizes the offset and length of the partition.
569
570 Subclasses can override this to provide better offset/length than what is
571 defined in the PGT partition layout.
572
573 Args:
574 offset: The offset (in bytes) of the partition in the image.
575 length: The length (in bytes) of the partition.
576
577 Returns:
578 A tuple of offset and length (in bytes) from the image.
579 """
580 return offset, length
Amin Hassanid684e982021-02-26 11:10:58 -0800581
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800582 def _RedirectPartition(self, file_name: str):
583 """Downloads the partition from a remote path and writes it into target.
584
585 Args:
586 file_name: The file name in the remote directory self._image.
587 """
588 cmd = self._GetWriteToTargetCommand()
589
590 image_path = os.path.join(self._image, file_name)
591 with GsFileCopier(image_path) as generator:
592 try:
593 with open(generator.Target(), 'rb') as fp:
594 self._device.run(cmd, input=fp, shell=True)
595 finally:
596 generator.CloseTarget()
597
Amin Hassanid684e982021-02-26 11:10:58 -0800598
599class KernelUpdater(RawPartitionUpdater):
600 """A class to update the kernel partition on a Chromium OS device."""
601
602 def _GetPartitionName(self):
603 """See RawPartitionUpdater._GetPartitionName()."""
604 return constants.PART_KERN_B
605
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800606 def _GetRemotePartitionName(self):
607 """See RawPartitionUpdater._GetRemotePartitionName()."""
608 return constants.QUICK_PROVISION_PAYLOAD_KERNEL
609
Amin Hassanid684e982021-02-26 11:10:58 -0800610 def Revert(self):
611 """Reverts the kernel partition update."""
612 # There is nothing to do for reverting kernel partition.
Amin Hassani75c5f942021-02-20 23:56:53 -0800613
614
615class RootfsUpdater(RawPartitionUpdater):
616 """A class to update the root partition on a Chromium OS device."""
617
618 def __init__(self, current_root: str, *args):
619 """Initializes the class.
620
621 Args:
622 current_root: The current root device path.
623 *args: See PartitionUpdaterBase
624 """
625 super().__init__(*args)
626
627 self._current_root = current_root
628 self._ran_postinst = False
629
630 def _GetPartitionName(self):
631 """See RawPartitionUpdater._GetPartitionName()."""
632 return constants.PART_ROOT_A
633
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800634 def _GetRemotePartitionName(self):
635 """See RawPartitionUpdater._GetRemotePartitionName()."""
636 return constants.QUICK_PROVISION_PAYLOAD_ROOTFS
637
Amin Hassani75c5f942021-02-20 23:56:53 -0800638 def _Run(self):
639 """The function that does the job of rootfs partition update."""
Amin Hassani55970562021-02-22 20:49:13 -0800640 with ProgressWatcher(self._device, self._target):
641 super()._Run()
Amin Hassani75c5f942021-02-20 23:56:53 -0800642
643 self._RunPostInst()
644
645 def _OptimizePartLocation(self, offset: int, length: int):
646 """Optimizes the size of the root partition of the image.
647
648 Normally the file system does not occupy the entire partition. Furthermore
649 we don't need the verity hash tree at the end of the root file system
650 because postinst will recreate it. This function reads the (approximate)
651 superblock of the ext4 partition and extracts the actual file system size in
652 the root partition.
653 """
654 superblock_size = 4096 * 2
655 with open(self._image, 'rb') as r:
656 r.seek(offset)
657 with tempfile.NamedTemporaryFile(delete=False) as fp:
658 fp.write(r.read(superblock_size))
659 fp.close()
660 return offset, partition_lib.Ext2FileSystemSize(fp.name)
661
662 def _RunPostInst(self, on_target: bool = True):
663 """Runs the postinst process in the root partition.
664
665 Args:
666 on_target: If true the postinst is run on the target (inactive)
667 partition. This is used when doing normal updates. If false, the
668 postinst is run on the current (active) partition. This is used when
669 reverting an update.
670 """
671 try:
672 postinst_dir = '/'
673 partition = self._current_root
674 if on_target:
675 postinst_dir = self._device.run(
676 ['mktemp', '-d', '-p', self._device.work_dir],
677 capture_output=True).stdout.strip()
678 self._device.run(['mount', '-o', 'ro', self._target, postinst_dir])
679 partition = self._target
680
681 self._ran_postinst = True
682 postinst = os.path.join(postinst_dir, 'postinst')
683 result = self._device.run([postinst, partition], capture_output=True)
684
685 logging.debug('Postinst result on %s: \n%s', postinst, result.stdout)
Amin Hassani55970562021-02-22 20:49:13 -0800686 # DeviceImagerOperation will look for this log.
Amin Hassani75c5f942021-02-20 23:56:53 -0800687 logging.info('Postinstall completed.')
688 finally:
689 if on_target:
690 self._device.run(['umount', postinst_dir])
691
692 def Revert(self):
693 """Reverts the root update install."""
694 logging.info('Reverting the rootfs partition update.')
695 if self._ran_postinst:
696 # We don't have to do anything for revert if we haven't changed the kernel
697 # priorities yet.
698 self._RunPostInst(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800699
700
701class StatefulPayloadGenerator(ReaderBase):
702 """A class for generating a stateful update payload in a separate thread."""
703 def __init__(self, image: str):
704 """Initializes that class.
705
706 Args:
707 image: The path to a local Chromium OS image.
708 """
709 super().__init__()
710 self._image = image
711
712 def run(self):
713 """Generates the stateful update and writes it into the output pipe."""
714 try:
715 paygen_stateful_payload_lib.GenerateStatefulPayload(
716 self._image, self._Source())
717 finally:
718 self._CloseSource()
719
720
721class StatefulUpdater(PartitionUpdaterBase):
722 """A class to update the stateful partition on a device."""
723 def __init__(self, clobber_stateful: bool, *args):
724 """Initializes the class
725
726 Args:
727 clobber_stateful: Whether to clobber the stateful or not.
728 *args: Look at PartitionUpdaterBase.
729 """
730 super().__init__(*args)
731 self._clobber_stateful = clobber_stateful
732
733 def _Run(self):
734 """Reads/Downloads the stateful updates and writes it into the device."""
735 if self._image_type == ImageType.FULL:
736 generator_cls = StatefulPayloadGenerator
737 elif self._image_type == ImageType.REMOTE_DIRECTORY:
738 generator_cls = GsFileCopier
739 self._image = os.path.join(self._image,
740 paygen_stateful_payload_lib.STATEFUL_FILE)
741 else:
742 raise ValueError(f'Invalid image type {self._image_type}')
743
744 with generator_cls(self._image) as generator:
745 try:
746 updater = stateful_updater.StatefulUpdater(self._device)
747 updater.Update(
748 generator.Target(),
749 is_payload_on_device=False,
750 update_type=(stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER if
751 self._clobber_stateful else None))
752 finally:
753 generator.CloseTarget()
754
755 def Revert(self):
756 """Reverts the stateful partition update."""
757 logging.info('Reverting the stateful update.')
758 stateful_updater.StatefulUpdater(self._device).Reset()
Amin Hassani55970562021-02-22 20:49:13 -0800759
760
761class ProgressWatcher(threading.Thread):
762 """A class used for watching the progress of rootfs update."""
763
764 def __init__(self, device, target_root: str):
765 """Initializes the class.
766
767 Args:
768 device: The ChromiumOSDevice to be updated.
769 target_root: The target root partition to monitor the progress of.
770 """
771 super().__init__()
772
773 self._device = device
774 self._target_root = target_root
775 self._exit = False
776
777 def __enter__(self):
778 """Starts the thread."""
779 self.start()
780 return self
781
782 def __exit__(self, *args, **kwargs):
783 """Exists the thread."""
784 self._exit = True
785 self.join()
786
787 def _ShouldExit(self):
788 return self._exit
789
790 def run(self):
791 """Monitors the progress of the target root partitions' update.
792
793 This is done by periodically, reading the fd position of the process that is
794 writing into the target partition and reporting it back. Then the position
795 is divided by the size of the block device to report an approximate
796 progress.
797 """
798 cmd = ['blockdev', '--getsize64', self._target_root]
799 output = self._device.run(cmd, capture_output=True).stdout.strip()
800 if output is None:
801 raise Error(f'Cannot get the block device size from {output}.')
802 dev_size = int(output)
803
804 # Using lsof to find out which process is writing to the target rootfs.
805 cmd = f'lsof 2>/dev/null | grep {self._target_root}'
806 while not self._ShouldExit():
807 try:
808 output = self._device.run(cmd, capture_output=True,
809 shell=True).stdout.strip()
810 if output:
811 break
812 except cros_build_lib.RunCommandError:
813 continue
814 finally:
815 time.sleep(1)
816
817 # Now that we know which process is writing to it, we can look the fdinfo of
818 # stdout of that process to get its offset. We're assuming there will be no
819 # seek, which is correct.
820 pid = output.split()[1]
821 cmd = ['cat', f'/proc/{pid}/fdinfo/1']
822 while not self._ShouldExit():
823 try:
824 output = self._device.run(cmd, capture_output=True).stdout.strip()
825 m = re.search(r'^pos:\s*(\d+)$', output, flags=re.M)
826 if m:
827 offset = int(m.group(1))
828 # DeviceImagerOperation will look for this log.
829 logging.info('RootFS progress: %f', offset/dev_size)
830 except cros_build_lib.RunCommandError:
831 continue
832 finally:
833 time.sleep(1)
834
835
836class DeviceImagerOperation(operation.ProgressBarOperation):
837 """A class to provide a progress bar for DeviceImager operation."""
838
839 def __init__(self):
840 """Initializes the class."""
841 super().__init__()
842
843 self._progress = 0.0
844
845 def ParseOutput(self, output=None):
846 """Override function to parse the output and provide progress.
847
848 Args:
849 output: The stderr or stdout.
850 """
851 output = self._stdout.read()
852 match = re.findall(r'RootFS progress: (\d+(?:\.\d+)?)', output)
853 if match:
854 progress = float(match[0])
855 self._progress = max(self._progress, progress)
856
857 # If postinstall completes, move half of the remaining progress.
858 if re.findall(r'Postinstall completed', output):
859 self._progress += (1.0 - self._progress) / 2
860
861 # While waiting for reboot, each time, move half of the remaining progress.
862 if re.findall(r'Unable to get new boot_id', output):
863 self._progress += (1.0 - self._progress) / 2
864
865 if re.findall(r'DeviceImager completed.', output):
866 self._progress = 1.0
867
868 self.ProgressBar(self._progress)