blob: dbf5acede37feb74e76872c050919c346a4539f4 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2015 The ChromiumOS Authors
David Pursellf1d16a62015-03-25 13:31:04 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Install/copy the image to the device."""
6
Mike Frysinger93e8ffa2019-07-03 20:24:18 -04007from __future__ import division
David Pursellf1d16a62015-03-25 13:31:04 -07008
Chris McDonald14ac61d2021-07-21 11:49:56 -06009import logging
David Pursellf1d16a62015-03-25 13:31:04 -070010import os
Ralph Nathan9b997232015-05-15 13:13:12 -070011import re
David Pursellf1d16a62015-03-25 13:31:04 -070012import shutil
David Pursellf1d16a62015-03-25 13:31:04 -070013
Amin Hassani153f9162021-02-22 20:48:31 -080014from chromite.cli import device_imager
Achuith Bhandarkaree1336f2020-04-18 11:44:09 +000015from chromite.cli.cros import cros_chrome_sdk
David Pursellf1d16a62015-03-25 13:31:04 -070016from chromite.lib import commandline
17from chromite.lib import cros_build_lib
David Pursellf1d16a62015-03-25 13:31:04 -070018from chromite.lib import dev_server_wrapper as ds_wrapper
Ralph Nathan872ea4d2015-05-05 18:04:56 -070019from chromite.lib import operation
David Pursellf1d16a62015-03-25 13:31:04 -070020from chromite.lib import osutils
Gilad Arnold1c8eda52015-05-04 22:32:38 -070021from chromite.lib import path_util
David Pursellf1d16a62015-03-25 13:31:04 -070022from chromite.lib import remote_access
23
24
Achuith Bhandarkaree1336f2020-04-18 11:44:09 +000025def GetDefaultBoard():
Alex Klein1699fab2022-09-08 08:46:06 -060026 """Look up default board.
Achuith Bhandarkaree1336f2020-04-18 11:44:09 +000027
Alex Klein1699fab2022-09-08 08:46:06 -060028 In a chrome checkout, return $SDK_BOARD. In a chromeos checkout,
29 return the contents of .default_board.
30 """
31 if path_util.DetermineCheckout().type == path_util.CHECKOUT_TYPE_GCLIENT:
32 return os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV)
33 return cros_build_lib.GetDefaultBoard()
Achuith Bhandarkaree1336f2020-04-18 11:44:09 +000034
35
Ralph Nathan9b997232015-05-15 13:13:12 -070036class UsbImagerOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -060037 """Progress bar for flashing image to operation."""
Ralph Nathan9b997232015-05-15 13:13:12 -070038
Alex Klein1699fab2022-09-08 08:46:06 -060039 def __init__(self, image):
40 super().__init__()
41 self._size = os.path.getsize(image)
42 self._transferred = 0
43 self._bytes = re.compile(r"(\d+) bytes")
Ralph Nathan9b997232015-05-15 13:13:12 -070044
Alex Klein1699fab2022-09-08 08:46:06 -060045 def _GetDDPid(self):
46 """Get the Pid of dd."""
47 try:
48 pids = cros_build_lib.run(
49 ["pgrep", "dd"],
50 capture_output=True,
51 print_cmd=False,
52 encoding="utf-8",
53 ).stdout
54 for pid in pids.splitlines():
55 if osutils.IsChildProcess(int(pid), name="dd"):
56 return int(pid)
57 return -1
58 except cros_build_lib.RunCommandError:
59 # If dd isn't still running, then we assume that it is finished.
60 return -1
Ralph Nathan9b997232015-05-15 13:13:12 -070061
Alex Klein1699fab2022-09-08 08:46:06 -060062 def _PingDD(self, dd_pid):
63 """Send USR1 signal to dd to get status update."""
64 try:
65 cmd = ["kill", "-USR1", str(dd_pid)]
66 cros_build_lib.sudo_run(cmd, print_cmd=False)
67 except cros_build_lib.RunCommandError:
68 # Here we assume that dd finished in the background.
69 return
Ralph Nathan9b997232015-05-15 13:13:12 -070070
Alex Klein1699fab2022-09-08 08:46:06 -060071 def ParseOutput(self, output=None):
72 """Parse the output of dd to update progress bar."""
73 dd_pid = self._GetDDPid()
74 if dd_pid == -1:
75 return
Ralph Nathan9b997232015-05-15 13:13:12 -070076
Alex Klein1699fab2022-09-08 08:46:06 -060077 self._PingDD(dd_pid)
Ralph Nathan9b997232015-05-15 13:13:12 -070078
Alex Klein1699fab2022-09-08 08:46:06 -060079 if output is None:
80 stdout = self._stdout.read()
81 stderr = self._stderr.read()
82 output = stdout + stderr
Ralph Nathan9b997232015-05-15 13:13:12 -070083
Alex Klein1699fab2022-09-08 08:46:06 -060084 match = self._bytes.search(output)
85 if match:
86 self._transferred = int(match.groups()[0])
Ralph Nathan9b997232015-05-15 13:13:12 -070087
Alex Klein1699fab2022-09-08 08:46:06 -060088 self.ProgressBar(self._transferred / self._size)
Ralph Nathan9b997232015-05-15 13:13:12 -070089
90
Mike Frysinger32759e42016-12-21 18:40:16 -050091def _IsFilePathGPTDiskImage(file_path, require_pmbr=False):
Alex Klein1699fab2022-09-08 08:46:06 -060092 """Determines if a file is a valid GPT disk.
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -070093
Alex Klein1699fab2022-09-08 08:46:06 -060094 Args:
95 file_path: Path to the file to test.
96 require_pmbr: Whether to require a PMBR in LBA0.
97 """
98 if os.path.isfile(file_path):
99 with open(file_path, "rb") as image_file:
100 if require_pmbr:
101 # Seek to the end of LBA0 and look for the PMBR boot signature.
102 image_file.seek(0x1FE)
103 if image_file.read(2) != b"\x55\xaa":
104 return False
105 # Current file position is start of LBA1 now.
106 else:
107 # Seek to LBA1 where the GPT starts.
108 image_file.seek(0x200)
Mike Frysinger32759e42016-12-21 18:40:16 -0500109
Alex Klein1699fab2022-09-08 08:46:06 -0600110 # See if there's a GPT here.
111 if image_file.read(8) == b"EFI PART":
112 return True
Mike Frysinger32759e42016-12-21 18:40:16 -0500113
Alex Klein1699fab2022-09-08 08:46:06 -0600114 return False
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700115
116
117def _ChooseImageFromDirectory(dir_path):
Alex Klein1699fab2022-09-08 08:46:06 -0600118 """Lists all image files in |dir_path| and ask user to select one.
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700119
Alex Klein1699fab2022-09-08 08:46:06 -0600120 Args:
121 dir_path: Path to the directory.
122 """
123 images = sorted(
124 [
125 x
126 for x in os.listdir(dir_path)
127 if _IsFilePathGPTDiskImage(os.path.join(dir_path, x))
128 ]
129 )
130 idx = 0
131 if not images:
132 raise ValueError("No image found in %s." % dir_path)
133 elif len(images) > 1:
134 idx = cros_build_lib.GetChoice(
135 "Multiple images found in %s. Please select one to continue:"
136 % ((dir_path,)),
137 images,
138 )
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700139
Alex Klein1699fab2022-09-08 08:46:06 -0600140 return os.path.join(dir_path, images[idx])
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700141
142
David Pursellf1d16a62015-03-25 13:31:04 -0700143class FlashError(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -0600144 """Thrown when there is an unrecoverable error during flash."""
David Pursellf1d16a62015-03-25 13:31:04 -0700145
146
147class USBImager(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600148 """Copy image to the target removable device."""
David Pursellf1d16a62015-03-25 13:31:04 -0700149
Alex Klein1699fab2022-09-08 08:46:06 -0600150 def __init__(self, device, board, image, version, debug=False, yes=False):
151 """Initializes USBImager."""
152 self.device = device
153 self.board = board if board else GetDefaultBoard()
154 self.image = image
155 self.version = version
156 self.debug = debug
157 self.debug_level = logging.DEBUG if debug else logging.INFO
158 self.yes = yes
David Pursellf1d16a62015-03-25 13:31:04 -0700159
Alex Klein1699fab2022-09-08 08:46:06 -0600160 def DeviceNameToPath(self, device_name):
161 return "/dev/%s" % device_name
David Pursellf1d16a62015-03-25 13:31:04 -0700162
Alex Klein1699fab2022-09-08 08:46:06 -0600163 def GetRemovableDeviceDescription(self, device):
164 """Returns a informational description of the removable |device|.
David Pursellf1d16a62015-03-25 13:31:04 -0700165
Alex Klein1699fab2022-09-08 08:46:06 -0600166 Args:
167 device: the device name (e.g. sdc).
David Pursellf1d16a62015-03-25 13:31:04 -0700168
Alex Klein1699fab2022-09-08 08:46:06 -0600169 Returns:
170 A string describing |device| (e.g. Patriot Memory 7918 MB).
171 """
172 desc = [
173 osutils.GetDeviceInfo(device, keyword="manufacturer"),
174 osutils.GetDeviceInfo(device, keyword="product"),
175 osutils.GetDeviceSize(self.DeviceNameToPath(device)),
176 "(%s)" % self.DeviceNameToPath(device),
177 ]
178 return " ".join([x for x in desc if x])
David Pursellf1d16a62015-03-25 13:31:04 -0700179
Alex Klein1699fab2022-09-08 08:46:06 -0600180 def ListAllRemovableDevices(self):
181 """Returns a list of removable devices.
David Pursellf1d16a62015-03-25 13:31:04 -0700182
Alex Klein1699fab2022-09-08 08:46:06 -0600183 Returns:
184 A list of device names (e.g. ['sdb', 'sdc']).
185 """
186 devices = osutils.ListBlockDevices()
187 removable_devices = []
188 for d in devices:
189 if d.TYPE == "disk" and (d.RM == "1" or d.HOTPLUG == "1"):
190 removable_devices.append(d.NAME)
David Pursellf1d16a62015-03-25 13:31:04 -0700191
Alex Klein1699fab2022-09-08 08:46:06 -0600192 return removable_devices
David Pursellf1d16a62015-03-25 13:31:04 -0700193
Alex Klein1699fab2022-09-08 08:46:06 -0600194 def ChooseRemovableDevice(self, devices):
195 """Lists all removable devices and asks user to select/confirm.
David Pursellf1d16a62015-03-25 13:31:04 -0700196
Alex Klein1699fab2022-09-08 08:46:06 -0600197 Args:
198 devices: a list of device names (e.g. ['sda', 'sdb']).
David Pursellf1d16a62015-03-25 13:31:04 -0700199
Alex Klein1699fab2022-09-08 08:46:06 -0600200 Returns:
201 The device name chosen by the user.
202 """
203 idx = cros_build_lib.GetChoice(
204 "Removable device(s) found. Please select/confirm to continue:",
205 [self.GetRemovableDeviceDescription(x) for x in devices],
206 )
David Pursellf1d16a62015-03-25 13:31:04 -0700207
Alex Klein1699fab2022-09-08 08:46:06 -0600208 return devices[idx]
David Pursellf1d16a62015-03-25 13:31:04 -0700209
Alex Klein1699fab2022-09-08 08:46:06 -0600210 def CopyImageToDevice(self, image, device):
211 """Copies |image| to the removable |device|.
David Pursellf1d16a62015-03-25 13:31:04 -0700212
Alex Klein1699fab2022-09-08 08:46:06 -0600213 Args:
214 image: Path to the image to copy.
215 device: Device to copy to.
216 """
217 cmd = [
218 "dd",
219 "if=%s" % image,
220 "of=%s" % device,
221 "bs=4M",
222 "iflag=fullblock",
223 "oflag=direct",
224 "conv=fdatasync",
225 ]
226 if logging.getLogger().getEffectiveLevel() <= logging.NOTICE:
227 op = UsbImagerOperation(image)
228 op.Run(
229 cros_build_lib.sudo_run,
230 cmd,
231 debug_level=logging.NOTICE,
232 encoding="utf-8",
233 update_period=0.5,
234 )
235 else:
236 cros_build_lib.sudo_run(
237 cmd,
238 debug_level=logging.NOTICE,
239 print_cmd=logging.getLogger().getEffectiveLevel()
240 < logging.NOTICE,
241 )
David Pursellf1d16a62015-03-25 13:31:04 -0700242
Alex Klein1699fab2022-09-08 08:46:06 -0600243 # dd likely didn't put the backup GPT in the last block. sfdisk fixes this
244 # up for us with a 'write' command, so we have a standards-conforming GPT.
245 # Ignore errors because sfdisk (util-linux < v2.32) isn't always happy to
246 # fix GPT correctness issues.
247 cros_build_lib.sudo_run(
248 ["sfdisk", device],
249 input="write\n",
250 check=False,
251 debug_level=self.debug_level,
252 )
Brian Norris6386fde2018-10-29 13:34:28 -0700253
Alex Klein1699fab2022-09-08 08:46:06 -0600254 cros_build_lib.sudo_run(
255 ["partx", "-u", device], debug_level=self.debug_level
256 )
257 cros_build_lib.sudo_run(
258 ["sync", "-d", device], debug_level=self.debug_level
259 )
David Pursellf1d16a62015-03-25 13:31:04 -0700260
Alex Klein1699fab2022-09-08 08:46:06 -0600261 def _GetImagePath(self):
262 """Returns the image path to use."""
263 image_path = None
264 if os.path.isfile(self.image):
265 if not self.yes and not _IsFilePathGPTDiskImage(self.image):
266 # TODO(wnwen): Open the tarball and if there is just one file in it,
267 # use that instead. Existing code in upload_symbols.py.
268 if cros_build_lib.BooleanPrompt(
269 prolog="The given image file is not a valid disk image. Perhaps "
270 "you forgot to untar it.",
271 prompt="Terminate the current flash process?",
272 ):
273 raise FlashError("Update terminated by user.")
274 image_path = self.image
275 elif os.path.isdir(self.image):
276 # Ask user which image (*.bin) in the folder to use.
277 image_path = _ChooseImageFromDirectory(self.image)
278 else:
279 # Translate the xbuddy path to get the exact image to use.
280 _, image_path = ds_wrapper.GetImagePathWithXbuddy(
281 self.image, self.board, self.version
282 )
David Pursellf1d16a62015-03-25 13:31:04 -0700283
Alex Klein1699fab2022-09-08 08:46:06 -0600284 logging.info("Using image %s", image_path)
285 return image_path
David Pursellf1d16a62015-03-25 13:31:04 -0700286
Alex Klein1699fab2022-09-08 08:46:06 -0600287 def Run(self):
288 """Image the removable device."""
289 devices = self.ListAllRemovableDevices()
David Pursellf1d16a62015-03-25 13:31:04 -0700290
Alex Klein1699fab2022-09-08 08:46:06 -0600291 if self.device:
292 # If user specified a device path, check if it exists.
293 if not os.path.exists(self.device):
294 raise FlashError("Device path %s does not exist." % self.device)
David Pursellf1d16a62015-03-25 13:31:04 -0700295
Alex Klein1699fab2022-09-08 08:46:06 -0600296 # Then check if it is removable.
297 if self.device not in [self.DeviceNameToPath(x) for x in devices]:
298 msg = "%s is not a removable device." % self.device
299 if not (
300 self.yes
301 or cros_build_lib.BooleanPrompt(default=False, prolog=msg)
302 ):
303 raise FlashError(
304 "You can specify usb:// to choose from a list of "
305 "removable devices."
306 )
307 target = None
308 if self.device:
309 # Get device name from path (e.g. sdc in /dev/sdc).
310 target = self.device.rsplit(os.path.sep, 1)[-1]
311 elif devices:
312 # Ask user to choose from the list.
313 target = self.ChooseRemovableDevice(devices)
314 else:
315 raise FlashError("No removable devices detected.")
David Pursellf1d16a62015-03-25 13:31:04 -0700316
Alex Klein1699fab2022-09-08 08:46:06 -0600317 image_path = self._GetImagePath()
318 device = self.DeviceNameToPath(target)
Jae Hoon Kim7f7bc232022-05-04 23:01:16 +0000319
Alex Klein1699fab2022-09-08 08:46:06 -0600320 device_size_bytes = osutils.GetDeviceSize(device, in_bytes=True)
321 image_size_bytes = os.path.getsize(image_path)
322 if device_size_bytes < image_size_bytes:
323 raise FlashError(
324 "Removable device %s has less storage (%d) than the image size (%d)."
325 % (device, device_size_bytes, image_size_bytes)
326 )
Jae Hoon Kim7f7bc232022-05-04 23:01:16 +0000327
Alex Klein1699fab2022-09-08 08:46:06 -0600328 try:
329 self.CopyImageToDevice(image_path, device)
330 except cros_build_lib.RunCommandError:
331 logging.error(
332 "Failed copying image to device %s",
333 self.DeviceNameToPath(target),
334 )
David Pursellf1d16a62015-03-25 13:31:04 -0700335
336
337class FileImager(USBImager):
Alex Klein1699fab2022-09-08 08:46:06 -0600338 """Copy image to the target path."""
David Pursellf1d16a62015-03-25 13:31:04 -0700339
Alex Klein1699fab2022-09-08 08:46:06 -0600340 def Run(self):
341 """Copy the image to the path specified by self.device."""
342 if not os.path.isdir(os.path.dirname(self.device)):
343 raise FlashError(
344 "Parent of path %s is not a directory." % self.device
345 )
David Pursellf1d16a62015-03-25 13:31:04 -0700346
Alex Klein1699fab2022-09-08 08:46:06 -0600347 image_path = self._GetImagePath()
348 if os.path.isdir(self.device):
349 logging.info(
350 "Copying to %s",
351 os.path.join(self.device, os.path.basename(image_path)),
352 )
353 else:
354 logging.info("Copying to %s", self.device)
355 try:
356 shutil.copy(image_path, self.device)
357 except IOError:
358 logging.error(
359 "Failed to copy image %s to %s", image_path, self.device
360 )
David Pursellf1d16a62015-03-25 13:31:04 -0700361
362
Jae Hoon Kimcc723e02021-08-16 21:03:21 +0000363# TODO(b/190631159, b/196056723): Change default of no_minios_update to |False|.
Alex Klein1699fab2022-09-08 08:46:06 -0600364def Flash(
365 device,
366 image,
367 board=None,
368 version=None,
369 no_rootfs_update=False,
370 no_stateful_update=False,
371 no_minios_update=True,
372 clobber_stateful=False,
373 reboot=True,
374 ssh_private_key=None,
375 ping=True,
376 disable_rootfs_verification=False,
377 clear_cache=False,
378 yes=False,
379 force=False,
380 debug=False,
381 clear_tpm_owner=False,
382 delta=False,
383):
384 """Flashes a device, USB drive, or file with an image.
David Pursellf1d16a62015-03-25 13:31:04 -0700385
Alex Klein1699fab2022-09-08 08:46:06 -0600386 This provides functionality common to `cros flash` and `brillo flash`
387 so that they can parse the commandline separately but still use the
388 same underlying functionality.
David Pursellf1d16a62015-03-25 13:31:04 -0700389
Alex Klein1699fab2022-09-08 08:46:06 -0600390 Args:
391 device: commandline.Device object; None to use the default device.
392 image: Path (string) to the update image. Can be a local or xbuddy path;
393 non-existant local paths are converted to xbuddy.
394 board: Board to use; None to automatically detect.
395 no_rootfs_update: Don't update rootfs partition; SSH |device| scheme only.
396 no_stateful_update: Don't update stateful partition; SSH |device| scheme
397 only.
398 no_minios_update: Don't update miniOS partition; SSH |device| scheme only.
399 clobber_stateful: Clobber stateful partition; SSH |device| scheme only.
400 clear_tpm_owner: Clear the TPM owner on reboot; SSH |device| scheme only.
401 reboot: Reboot device after update; SSH |device| scheme only.
402 ssh_private_key: Path to an SSH private key file; None to use test keys.
403 ping: Ping the device before attempting update; SSH |device| scheme only.
404 disable_rootfs_verification: Remove rootfs verification after update; SSH
405 |device| scheme only.
406 clear_cache: Clear the devserver static directory.
407 yes: Assume "yes" for any prompt.
408 force: Ignore confidence checks and prompts. Overrides |yes| if True.
409 debug: Print additional debugging messages.
410 version: Default version.
411 delta: Whether to use delta compression when tranferring image bytes.
David Pursellf1d16a62015-03-25 13:31:04 -0700412
Alex Klein1699fab2022-09-08 08:46:06 -0600413 Raises:
414 FlashError: An unrecoverable error occured.
415 ValueError: Invalid parameter combination.
416 """
417 if force:
418 yes = True
David Pursellf1d16a62015-03-25 13:31:04 -0700419
Alex Klein1699fab2022-09-08 08:46:06 -0600420 if clear_cache:
421 ds_wrapper.DevServerWrapper.WipeStaticDirectory()
422 ds_wrapper.DevServerWrapper.CreateStaticDirectory()
David Pursellf1d16a62015-03-25 13:31:04 -0700423
Alex Klein1699fab2022-09-08 08:46:06 -0600424 # The user may not have specified a source image, use version as the default.
425 image = image or version
426 if not device or device.scheme == commandline.DEVICE_SCHEME_SSH:
427 if device:
428 hostname, port = device.hostname, device.port
429 else:
430 hostname, port = None, None
Amin Hassani153f9162021-02-22 20:48:31 -0800431
Alex Klein1699fab2022-09-08 08:46:06 -0600432 with remote_access.ChromiumOSDeviceHandler(
433 hostname, port=port, private_key=ssh_private_key, ping=ping
434 ) as device_p:
435 device_imager.DeviceImager(
436 device_p,
437 image,
438 board=board,
439 version=version,
440 no_rootfs_update=no_rootfs_update,
441 no_stateful_update=no_stateful_update,
442 no_minios_update=no_minios_update,
443 no_reboot=not reboot,
444 disable_verification=disable_rootfs_verification,
445 clobber_stateful=clobber_stateful,
446 clear_tpm_owner=clear_tpm_owner,
447 delta=delta,
448 ).Run()
449 elif device.scheme == commandline.DEVICE_SCHEME_USB:
450 path = osutils.ExpandPath(device.path) if device.path else ""
451 logging.info("Preparing to image the removable device %s", path)
452 imager = USBImager(path, board, image, version, debug=debug, yes=yes)
453 imager.Run()
454 elif device.scheme == commandline.DEVICE_SCHEME_FILE:
455 logging.info("Preparing to copy image to %s", device.path)
456 imager = FileImager(
457 device.path, board, image, version, debug=debug, yes=yes
458 )
459 imager.Run()