blob: 11931d73873f01b14781369dd8d940d1c7b50e4d [file] [log] [blame]
David Pursellf1d16a62015-03-25 13:31:04 -07001# Copyright 2015 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"""Install/copy the image to the device."""
6
7from __future__ import print_function
8
9import cStringIO
10import os
11import shutil
12import tempfile
13import time
14
15from chromite.cbuildbot import constants
Ralph Nathan872ea4d2015-05-05 18:04:56 -070016from chromite.cli import command
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -070017from chromite.lib import blueprint_lib
David Pursellf1d16a62015-03-25 13:31:04 -070018from chromite.lib import brick_lib
19from chromite.lib import commandline
20from chromite.lib import cros_build_lib
21from chromite.lib import cros_logging as logging
22from chromite.lib import dev_server_wrapper as ds_wrapper
Ralph Nathan872ea4d2015-05-05 18:04:56 -070023from chromite.lib import operation
David Pursellf1d16a62015-03-25 13:31:04 -070024from chromite.lib import osutils
25from chromite.lib import project_sdk
26from chromite.lib import remote_access
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -070027from chromite.lib import workspace_lib
David Pursellf1d16a62015-03-25 13:31:04 -070028
29
30_DEVSERVER_STATIC_DIR = cros_build_lib.FromChrootPath(
31 os.path.join(constants.CHROOT_SOURCE_ROOT, 'devserver', 'static'))
32
33
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -070034def _IsFilePathGPTDiskImage(file_path):
35 """Determines if a file is a valid GPT disk.
36
37 Args:
38 file_path: Path to the file to test.
39 """
40 if os.path.isfile(file_path):
41 with cros_build_lib.Open(file_path) as image_file:
42 image_file.seek(0x1fe)
43 if image_file.read(10) == '\x55\xaaEFI PART':
44 return True
45 return False
46
47
48def _ChooseImageFromDirectory(dir_path):
49 """Lists all image files in |dir_path| and ask user to select one.
50
51 Args:
52 dir_path: Path to the directory.
53 """
54 images = sorted([x for x in os.listdir(dir_path) if
55 _IsFilePathGPTDiskImage(os.path.join(dir_path, x))])
56 idx = 0
57 if len(images) == 0:
58 raise ValueError('No image found in %s.' % dir_path)
59 elif len(images) > 1:
60 idx = cros_build_lib.GetChoice(
61 'Multiple images found in %s. Please select one to continue:' % (
62 (dir_path,)),
63 images)
64
65 return os.path.join(dir_path, images[idx])
66
67
David Pursellf1d16a62015-03-25 13:31:04 -070068class FlashError(Exception):
69 """Thrown when there is an unrecoverable error during flash."""
70
71
72class USBImager(object):
73 """Copy image to the target removable device."""
74
75 def __init__(self, device, board, image, sdk_version=None, debug=False,
76 install=False, yes=False):
77 """Initalizes USBImager."""
78 self.device = device
79 self.board = board if board else cros_build_lib.GetDefaultBoard()
80 self.image = image
81 self.sdk_version = sdk_version
82 self.debug = debug
83 self.debug_level = logging.DEBUG if debug else logging.INFO
84 self.install = install
85 self.yes = yes
86
87 def DeviceNameToPath(self, device_name):
88 return '/dev/%s' % device_name
89
90 def GetRemovableDeviceDescription(self, device):
91 """Returns a informational description of the removable |device|.
92
93 Args:
94 device: the device name (e.g. sdc).
95
96 Returns:
97 A string describing |device| (e.g. Patriot Memory 7918 MB).
98 """
99 desc = [
100 osutils.GetDeviceInfo(device, keyword='manufacturer'),
101 osutils.GetDeviceInfo(device, keyword='product'),
102 osutils.GetDeviceSize(self.DeviceNameToPath(device)),
103 '(%s)' % self.DeviceNameToPath(device),
104 ]
105 return ' '.join([x for x in desc if x])
106
107 def ListAllRemovableDevices(self):
108 """Returns a list of removable devices.
109
110 Returns:
111 A list of device names (e.g. ['sdb', 'sdc']).
112 """
113 devices = osutils.ListBlockDevices()
114 removable_devices = []
115 for d in devices:
116 if d.TYPE == 'disk' and d.RM == '1':
117 removable_devices.append(d.NAME)
118
119 return removable_devices
120
121 def ChooseRemovableDevice(self, devices):
122 """Lists all removable devices and asks user to select/confirm.
123
124 Args:
125 devices: a list of device names (e.g. ['sda', 'sdb']).
126
127 Returns:
128 The device name chosen by the user.
129 """
130 idx = cros_build_lib.GetChoice(
131 'Removable device(s) found. Please select/confirm to continue:',
132 [self.GetRemovableDeviceDescription(x) for x in devices])
133
134 return devices[idx]
135
136 def InstallImageToDevice(self, image, device):
137 """Installs |image| to the removable |device|.
138
139 Args:
140 image: Path to the image to copy.
141 device: Device to copy to.
142 """
143 cmd = [
144 'chromeos-install',
145 '--yes',
146 '--skip_src_removable',
147 '--skip_dst_removable',
148 '--payload_image=%s' % image,
149 '--dst=%s' % device,
150 '--skip_postinstall',
151 ]
152 cros_build_lib.SudoRunCommand(cmd)
153
154 def CopyImageToDevice(self, image, device):
155 """Copies |image| to the removable |device|.
156
157 Args:
158 image: Path to the image to copy.
159 device: Device to copy to.
160 """
161 # Use pv to display progress bar if possible.
162 cmd_base = 'pv -pretb'
163 try:
164 cros_build_lib.RunCommand(['pv', '--version'], print_cmd=False,
165 capture_output=True)
166 except cros_build_lib.RunCommandError:
167 cmd_base = 'cat'
168
169 cmd = '%s %s | dd of=%s bs=4M iflag=fullblock oflag=sync' % (
170 cmd_base, image, device)
Ralph Nathanf2c1d792015-05-05 17:16:15 -0700171
172 # We want to display the output at logging level NOTICE or less but we only
173 # want to print the command at logging levels INFO and DEBUG.
174 cros_build_lib.SudoRunCommand(
175 cmd, shell=True, debug_level=logging.NOTICE,
176 print_cmd=logging.getLogger().getEffectiveLevel() < logging.NOTICE)
David Pursellf1d16a62015-03-25 13:31:04 -0700177 cros_build_lib.SudoRunCommand(['sync'], debug_level=self.debug_level)
178
David Pursellf1d16a62015-03-25 13:31:04 -0700179
180 def _GetImagePath(self):
181 """Returns the image path to use."""
182 image_path = translated_path = None
183 if os.path.isfile(self.image):
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700184 if not self.yes and not _IsFilePathGPTDiskImage(self.image):
David Pursellf1d16a62015-03-25 13:31:04 -0700185 # TODO(wnwen): Open the tarball and if there is just one file in it,
186 # use that instead. Existing code in upload_symbols.py.
187 if cros_build_lib.BooleanPrompt(
188 prolog='The given image file is not a valid disk image. Perhaps '
189 'you forgot to untar it.',
190 prompt='Terminate the current flash process?'):
191 raise FlashError('Update terminated by user.')
192 image_path = self.image
193 elif os.path.isdir(self.image):
194 # Ask user which image (*.bin) in the folder to use.
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700195 image_path = _ChooseImageFromDirectory(self.image)
David Pursellf1d16a62015-03-25 13:31:04 -0700196 else:
197 # Translate the xbuddy path to get the exact image to use.
Gilad Arnolde62ec902015-04-24 14:41:02 -0700198 translated_path, _ = ds_wrapper.GetImagePathWithXbuddy(
David Pursellf1d16a62015-03-25 13:31:04 -0700199 self.image, self.board, version=self.sdk_version,
200 static_dir=_DEVSERVER_STATIC_DIR)
201 image_path = ds_wrapper.TranslatedPathToLocalPath(
202 translated_path, _DEVSERVER_STATIC_DIR)
203
204 logging.info('Using image %s', translated_path or image_path)
205 return image_path
206
207 def Run(self):
208 """Image the removable device."""
209 devices = self.ListAllRemovableDevices()
210
211 if self.device:
212 # If user specified a device path, check if it exists.
213 if not os.path.exists(self.device):
214 raise FlashError('Device path %s does not exist.' % self.device)
215
216 # Then check if it is removable.
217 if self.device not in [self.DeviceNameToPath(x) for x in devices]:
218 msg = '%s is not a removable device.' % self.device
219 if not (self.yes or cros_build_lib.BooleanPrompt(
220 default=False, prolog=msg)):
221 raise FlashError('You can specify usb:// to choose from a list of '
222 'removable devices.')
223 target = None
224 if self.device:
225 # Get device name from path (e.g. sdc in /dev/sdc).
226 target = self.device.rsplit(os.path.sep, 1)[-1]
227 elif devices:
228 # Ask user to choose from the list.
229 target = self.ChooseRemovableDevice(devices)
230 else:
231 raise FlashError('No removable devices detected.')
232
233 image_path = self._GetImagePath()
234 try:
235 device = self.DeviceNameToPath(target)
236 if self.install:
237 self.InstallImageToDevice(image_path, device)
238 else:
239 self.CopyImageToDevice(image_path, device)
240 except cros_build_lib.RunCommandError:
241 logging.error('Failed copying image to device %s',
242 self.DeviceNameToPath(target))
243
244
245class FileImager(USBImager):
246 """Copy image to the target path."""
247
248 def Run(self):
249 """Copy the image to the path specified by self.device."""
250 if not os.path.exists(self.device):
251 raise FlashError('Path %s does not exist.' % self.device)
252
253 image_path = self._GetImagePath()
254 if os.path.isdir(self.device):
255 logging.info('Copying to %s',
256 os.path.join(self.device, os.path.basename(image_path)))
257 else:
258 logging.info('Copying to %s', self.device)
259 try:
260 shutil.copy(image_path, self.device)
261 except IOError:
262 logging.error('Failed to copy image %s to %s', image_path, self.device)
263
264
265class RemoteDeviceUpdater(object):
266 """Performs update on a remote device."""
267 DEVSERVER_FILENAME = 'devserver.py'
268 STATEFUL_UPDATE_BIN = '/usr/bin/stateful_update'
269 UPDATE_ENGINE_BIN = 'update_engine_client'
David Pursellf1d16a62015-03-25 13:31:04 -0700270 # Root working directory on the device. This directory is in the
271 # stateful partition and thus has enough space to store the payloads.
272 DEVICE_BASE_DIR = '/mnt/stateful_partition/cros-flash'
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700273 UPDATE_CHECK_INTERVAL_PROGRESSBAR = 0.5
274 UPDATE_CHECK_INTERVAL_NORMAL = 10
David Pursellf1d16a62015-03-25 13:31:04 -0700275
276 def __init__(self, ssh_hostname, ssh_port, image, stateful_update=True,
277 rootfs_update=True, clobber_stateful=False, reboot=True,
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700278 board=None, src_image_to_delta=None, wipe=True, debug=False,
279 yes=False, force=False, ping=True, disable_verification=False,
280 sdk_version=None):
David Pursellf1d16a62015-03-25 13:31:04 -0700281 """Initializes RemoteDeviceUpdater"""
282 if not stateful_update and not rootfs_update:
283 raise ValueError('No update operation to perform; either stateful or'
284 ' rootfs partitions must be updated.')
285 self.tempdir = tempfile.mkdtemp(prefix='cros-flash')
286 self.ssh_hostname = ssh_hostname
287 self.ssh_port = ssh_port
288 self.image = image
289 self.board = board
David Pursellf1d16a62015-03-25 13:31:04 -0700290 self.src_image_to_delta = src_image_to_delta
291 self.do_stateful_update = stateful_update
292 self.do_rootfs_update = rootfs_update
293 self.disable_verification = disable_verification
294 self.clobber_stateful = clobber_stateful
295 self.reboot = reboot
296 self.debug = debug
297 self.ping = ping
298 # Do not wipe if debug is set.
299 self.wipe = wipe and not debug
300 self.yes = yes
301 self.force = force
302 self.sdk_version = sdk_version
303
304 # pylint: disable=unbalanced-tuple-unpacking
305 @classmethod
306 def GetUpdateStatus(cls, device, keys=None):
307 """Returns the status of the update engine on the |device|.
308
309 Retrieves the status from update engine and confirms all keys are
310 in the status.
311
312 Args:
313 device: A ChromiumOSDevice object.
314 keys: the keys to look for in the status result (defaults to
315 ['CURRENT_OP']).
316
317 Returns:
318 A list of values in the order of |keys|.
319 """
320 keys = ['CURRENT_OP'] if not keys else keys
321 result = device.RunCommand([cls.UPDATE_ENGINE_BIN, '--status'],
322 capture_output=True)
323 if not result.output:
324 raise Exception('Cannot get update status')
325
326 try:
327 status = cros_build_lib.LoadKeyValueFile(
328 cStringIO.StringIO(result.output))
329 except ValueError:
330 raise ValueError('Cannot parse update status')
331
332 values = []
333 for key in keys:
334 if key not in status:
335 raise ValueError('Missing %s in the update engine status')
336
337 values.append(status.get(key))
338
339 return values
340
341 def UpdateStateful(self, device, payload, clobber=False):
342 """Update the stateful partition of the device.
343
344 Args:
345 device: The ChromiumOSDevice object to update.
346 payload: The path to the update payload.
347 clobber: Clobber stateful partition (defaults to False).
348 """
349 # Copy latest stateful_update to device.
350 stateful_update_bin = cros_build_lib.FromChrootPath(
351 self.STATEFUL_UPDATE_BIN)
352 device.CopyToWorkDir(stateful_update_bin)
353 msg = 'Updating stateful partition'
354 logging.info('Copying stateful payload to device...')
355 device.CopyToWorkDir(payload)
356 cmd = ['sh',
357 os.path.join(device.work_dir,
358 os.path.basename(self.STATEFUL_UPDATE_BIN)),
359 os.path.join(device.work_dir, os.path.basename(payload))]
360
361 if clobber:
362 cmd.append('--stateful_change=clean')
363 msg += ' with clobber enabled'
364
365 logging.info('%s...', msg)
366 try:
367 device.RunCommand(cmd)
368 except cros_build_lib.RunCommandError:
369 logging.error('Faild to perform stateful partition update.')
370
371 def _CopyDevServerPackage(self, device, tempdir):
372 """Copy devserver package to work directory of device.
373
374 Args:
375 device: The ChromiumOSDevice object to copy the package to.
376 tempdir: The directory to temporarily store devserver package.
377 """
378 logging.info('Copying devserver package to device...')
379 src_dir = os.path.join(tempdir, 'src')
380 osutils.RmDir(src_dir, ignore_missing=True)
381 shutil.copytree(
382 ds_wrapper.DEVSERVER_PKG_DIR, src_dir,
383 ignore=shutil.ignore_patterns('*.pyc', 'tmp*', '.*', 'static', '*~'))
384 device.CopyToWorkDir(src_dir)
385 return os.path.join(device.work_dir, os.path.basename(src_dir))
386
387 def SetupRootfsUpdate(self, device):
388 """Makes sure |device| is ready for rootfs update."""
389 logging.info('Checking if update engine is idle...')
390 status, = self.GetUpdateStatus(device)
391 if status == 'UPDATE_STATUS_UPDATED_NEED_REBOOT':
392 logging.info('Device needs to reboot before updating...')
393 device.Reboot()
394 status, = self.GetUpdateStatus(device)
395
396 if status != 'UPDATE_STATUS_IDLE':
397 raise FlashError('Update engine is not idle. Status: %s' % status)
398
399 def UpdateRootfs(self, device, payload, tempdir):
400 """Update the rootfs partition of the device.
401
402 Args:
403 device: The ChromiumOSDevice object to update.
404 payload: The path to the update payload.
405 tempdir: The directory to store temporary files.
406 """
407 # Setup devserver and payload on the target device.
408 static_dir = os.path.join(device.work_dir, 'static')
409 payload_dir = os.path.join(static_dir, 'pregenerated')
410 src_dir = self._CopyDevServerPackage(device, tempdir)
411 device.RunCommand(['mkdir', '-p', payload_dir])
412 logging.info('Copying rootfs payload to device...')
413 device.CopyToDevice(payload, payload_dir)
414 devserver_bin = os.path.join(src_dir, self.DEVSERVER_FILENAME)
415 ds = ds_wrapper.RemoteDevServerWrapper(
416 device, devserver_bin, static_dir=static_dir, log_dir=device.work_dir)
417
418 logging.info('Updating rootfs partition')
419 try:
420 ds.Start()
421 # Use the localhost IP address to ensure that update engine
422 # client can connect to the devserver.
423 omaha_url = ds.GetDevServerURL(
424 ip='127.0.0.1', port=ds.port, sub_dir='update/pregenerated')
425 cmd = [self.UPDATE_ENGINE_BIN, '-check_for_update',
426 '-omaha_url=%s' % omaha_url]
427 device.RunCommand(cmd)
428
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700429 # If we are using a progress bar, update it every 0.5s instead of 10s.
430 if command.UseProgressBar():
431 update_check_interval = self.UPDATE_CHECK_INTERVAL_PROGRESSBAR
432 oper = operation.ProgressBarOperation()
433 else:
434 update_check_interval = self.UPDATE_CHECK_INTERVAL_NORMAL
435 oper = None
436 end_message_not_printed = True
437
David Pursellf1d16a62015-03-25 13:31:04 -0700438 # Loop until update is complete.
439 while True:
440 op, progress = self.GetUpdateStatus(device, ['CURRENT_OP', 'PROGRESS'])
441 logging.info('Waiting for update...status: %s at progress %s',
442 op, progress)
443
444 if op == 'UPDATE_STATUS_UPDATED_NEED_REBOOT':
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700445 logging.notice('Update completed.')
David Pursellf1d16a62015-03-25 13:31:04 -0700446 break
447
448 if op == 'UPDATE_STATUS_IDLE':
449 raise FlashError(
450 'Update failed with unexpected update status: %s' % op)
451
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700452 if oper is not None:
453 if op == 'UPDATE_STATUS_DOWNLOADING':
454 oper.ProgressBar(float(progress))
455 elif end_message_not_printed and op == 'UPDATE_STATUS_FINALIZING':
456 oper.Cleanup()
457 logging.notice('Finalizing image.')
458 end_message_not_printed = False
459
460 time.sleep(update_check_interval)
David Pursellf1d16a62015-03-25 13:31:04 -0700461
462 ds.Stop()
463 except Exception:
464 logging.error('Rootfs update failed.')
465 logging.warning(ds.TailLog() or 'No devserver log is available.')
466 raise
467 finally:
468 ds.Stop()
469 device.CopyFromDevice(ds.log_file,
470 os.path.join(tempdir, 'target_devserver.log'),
471 error_code_ok=True)
472 device.CopyFromDevice('/var/log/update_engine.log', tempdir,
473 follow_symlinks=True,
474 error_code_ok=True)
475
476 def _CheckPayloads(self, payload_dir):
477 """Checks that all update payloads exists in |payload_dir|."""
478 filenames = []
479 filenames += [ds_wrapper.ROOTFS_FILENAME] if self.do_rootfs_update else []
480 if self.do_stateful_update:
481 filenames += [ds_wrapper.STATEFUL_FILENAME]
482 for fname in filenames:
483 payload = os.path.join(payload_dir, fname)
484 if not os.path.exists(payload):
485 raise FlashError('Payload %s does not exist!' % payload)
486
487 def Verify(self, old_root_dev, new_root_dev):
488 """Verifies that the root deivce changed after reboot."""
489 assert new_root_dev and old_root_dev
490 if new_root_dev == old_root_dev:
491 raise FlashError(
492 'Failed to boot into the new version. Possibly there was a '
493 'signing problem, or an automated rollback occurred because '
494 'your new image failed to boot.')
495
496 @classmethod
497 def GetRootDev(cls, device):
498 """Get the current root device on |device|."""
499 rootdev = device.RunCommand(
500 ['rootdev', '-s'], capture_output=True).output.strip()
501 logging.debug('Current root device is %s', rootdev)
502 return rootdev
503
504 def Cleanup(self):
505 """Cleans up the temporary directory."""
506 if self.wipe:
507 logging.info('Cleaning up temporary working directory...')
508 osutils.RmDir(self.tempdir)
509 else:
510 logging.info('You can find the log files and/or payloads in %s',
511 self.tempdir)
512
513 def _CanRunDevserver(self, device, tempdir):
514 """We can run devserver on |device|.
515
516 If the stateful partition is corrupted, Python or other packages
517 (e.g. cherrypy) needed for rootfs update may be missing on |device|.
518
519 This will also use `ldconfig` to update library paths on the target
520 device if it looks like that's causing problems, which is necessary
521 for base images.
522
523 Args:
Mike Frysinger6f3c48e2015-05-06 02:38:51 -0400524 device: A ChromiumOSDevice object.
525 tempdir: A temporary directory to store files.
David Pursellf1d16a62015-03-25 13:31:04 -0700526
527 Returns:
528 True if we can start devserver; False otherwise.
529 """
530 logging.info('Checking if we can run devserver on the device.')
531 src_dir = self._CopyDevServerPackage(device, tempdir)
532 devserver_bin = os.path.join(src_dir, self.DEVSERVER_FILENAME)
533 devserver_check_command = ['python', devserver_bin, '--help']
534 try:
535 device.RunCommand(devserver_check_command)
536 except cros_build_lib.RunCommandError as e:
537 logging.warning('Cannot start devserver: %s', e)
538 if 'python: error while loading shared libraries' in str(e):
539 logging.info('Attempting to correct device library paths...')
540 try:
541 device.RunCommand(['ldconfig', '-r', '/'])
542 device.RunCommand(devserver_check_command)
543 logging.info('Library path correction successful.')
544 return True
545 except cros_build_lib.RunCommandError as e2:
546 logging.warning('Library path correction failed: %s', e2)
547
548 return False
549
550 return True
551
552 def Run(self):
553 """Performs remote device update."""
554 old_root_dev, new_root_dev = None, None
555 try:
556 device_connected = False
557 with remote_access.ChromiumOSDeviceHandler(
558 self.ssh_hostname, port=self.ssh_port,
559 base_dir=self.DEVICE_BASE_DIR, ping=self.ping) as device:
560 device_connected = True
561
David Pursellf1d16a62015-03-25 13:31:04 -0700562 payload_dir = self.tempdir
563 if os.path.isdir(self.image):
564 # If the given path is a directory, we use the provided
565 # update payload(s) in the directory.
566 payload_dir = self.image
567 logging.info('Using provided payloads in %s', payload_dir)
568 else:
569 if os.path.isfile(self.image):
570 # If the given path is an image, make sure devserver can
571 # access it and generate payloads.
572 logging.info('Using image %s', self.image)
573 ds_wrapper.GetUpdatePayloadsFromLocalPath(
574 self.image, payload_dir,
575 src_image_to_delta=self.src_image_to_delta,
576 static_dir=_DEVSERVER_STATIC_DIR)
577 else:
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700578 # We should ignore the given/inferred board value and stick to the
579 # device's basic designation. We do emit a warning for good
580 # measure.
581 # TODO(garnold) In fact we should find the board/overlay that the
582 # device inherits from and which defines the SDK "baseline" image
583 # (brillo:339).
584 if self.sdk_version and self.board and not self.force:
585 logging.warning(
586 'Ignoring board value (%s) and deferring to device; use '
587 '--force to override',
588 self.board)
589 self.board = None
590
591 self.board = cros_build_lib.GetBoard(device_board=device.board,
592 override_board=self.board,
593 force=self.yes)
594 if not self.board:
595 raise FlashError('No board identified')
596
597 if not self.force and self.board != device.board:
598 # If a board was specified, it must be compatible with the device.
599 raise FlashError('Device (%s) is incompatible with board %s',
600 device.board, self.board)
601
602 logging.info('Board is %s', self.board)
603
David Pursellf1d16a62015-03-25 13:31:04 -0700604 # Translate the xbuddy path to get the exact image to use.
Gilad Arnolde62ec902015-04-24 14:41:02 -0700605 translated_path, resolved_path = ds_wrapper.GetImagePathWithXbuddy(
David Pursellf1d16a62015-03-25 13:31:04 -0700606 self.image, self.board, version=self.sdk_version,
607 static_dir=_DEVSERVER_STATIC_DIR, lookup_only=True)
608 logging.info('Using image %s', translated_path)
609 # Convert the translated path to be used in the update request.
Gilad Arnolde62ec902015-04-24 14:41:02 -0700610 image_path = ds_wrapper.ConvertTranslatedPath(resolved_path,
David Pursellf1d16a62015-03-25 13:31:04 -0700611 translated_path)
612
613 # Launch a local devserver to generate/serve update payloads.
614 ds_wrapper.GetUpdatePayloads(
615 image_path, payload_dir, board=self.board,
616 src_image_to_delta=self.src_image_to_delta,
617 static_dir=_DEVSERVER_STATIC_DIR)
618
619 # Verify that all required payloads are in the payload directory.
620 self._CheckPayloads(payload_dir)
621
622 restore_stateful = False
623 if (not self._CanRunDevserver(device, self.tempdir) and
624 self.do_rootfs_update):
625 msg = ('Cannot start devserver! The stateful partition may be '
626 'corrupted.')
627 prompt = 'Attempt to restore the stateful partition?'
628 restore_stateful = self.yes or cros_build_lib.BooleanPrompt(
629 prompt=prompt, default=False, prolog=msg)
630 if not restore_stateful:
631 raise FlashError('Cannot continue to perform rootfs update!')
632
633 if restore_stateful:
634 logging.warning('Restoring the stateful partition...')
635 payload = os.path.join(payload_dir, ds_wrapper.STATEFUL_FILENAME)
636 self.UpdateStateful(device, payload, clobber=self.clobber_stateful)
637 device.Reboot()
638 if self._CanRunDevserver(device, self.tempdir):
639 logging.info('Stateful partition restored.')
640 else:
641 raise FlashError('Unable to restore stateful partition.')
642
643 # Perform device updates.
644 if self.do_rootfs_update:
645 self.SetupRootfsUpdate(device)
646 # Record the current root device. This must be done after
647 # SetupRootfsUpdate because SetupRootfsUpdate may reboot the
648 # device if there is a pending update, which changes the
649 # root device.
650 old_root_dev = self.GetRootDev(device)
651 payload = os.path.join(payload_dir, ds_wrapper.ROOTFS_FILENAME)
652 self.UpdateRootfs(device, payload, self.tempdir)
653 logging.info('Rootfs update completed.')
654
655 if self.do_stateful_update and not restore_stateful:
656 payload = os.path.join(payload_dir, ds_wrapper.STATEFUL_FILENAME)
657 self.UpdateStateful(device, payload, clobber=self.clobber_stateful)
658 logging.info('Stateful update completed.')
659
660 if self.reboot:
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700661 logging.notice('Rebooting device...')
David Pursellf1d16a62015-03-25 13:31:04 -0700662 device.Reboot()
663 if self.clobber_stateful:
664 # --clobber-stateful wipes the stateful partition and the
665 # working directory on the device no longer exists. To
666 # remedy this, we recreate the working directory here.
667 device.BaseRunCommand(['mkdir', '-p', device.work_dir])
668
669 if self.do_rootfs_update and self.reboot:
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700670 logging.notice('Verifying that the device has been updated...')
David Pursellf1d16a62015-03-25 13:31:04 -0700671 new_root_dev = self.GetRootDev(device)
672 self.Verify(old_root_dev, new_root_dev)
673
674 if self.disable_verification:
675 logging.info('Disabling rootfs verification on the device...')
676 device.DisableRootfsVerification()
677
678 except Exception:
679 logging.error('Device update failed.')
680 if device_connected and device.lsb_release:
681 lsb_entries = sorted(device.lsb_release.items())
682 logging.info('Following are the LSB version details of the device:\n%s',
683 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
684 raise
685 else:
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700686 logging.notice('Update performed successfully.')
David Pursellf1d16a62015-03-25 13:31:04 -0700687 finally:
688 self.Cleanup()
689
690
691# TODO(dpursell): replace |brick| argument with blueprints when they're ready.
Gilad Arnold94fc3b02015-04-29 13:04:32 -0700692def Flash(device, image, project_sdk_image=False, sdk_version=None, board=None,
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700693 brick_name=None, blueprint_name=None, install=False,
694 src_image_to_delta=None, rootfs_update=True, stateful_update=True,
695 clobber_stateful=False, reboot=True, wipe=True, ping=True,
696 disable_rootfs_verification=False, clear_cache=False, yes=False,
697 force=False, debug=False):
David Pursellf1d16a62015-03-25 13:31:04 -0700698 """Flashes a device, USB drive, or file with an image.
699
700 This provides functionality common to `cros flash` and `brillo flash`
701 so that they can parse the commandline separately but still use the
702 same underlying functionality.
703
704 Args:
David Pursell2e773382015-04-03 14:30:47 -0700705 device: commandline.Device object; None to use the default device.
David Pursellf1d16a62015-03-25 13:31:04 -0700706 image: Path (string) to the update image. Can be a local or xbuddy path;
707 non-existant local paths are converted to xbuddy.
708 project_sdk_image: Use a clean project SDK image. Overrides |image| if True.
Gilad Arnold94fc3b02015-04-29 13:04:32 -0700709 sdk_version: Which version of SDK image to flash; autodetected if None.
David Pursellf1d16a62015-03-25 13:31:04 -0700710 board: Board to use; None to automatically detect.
Gilad Arnold23bfb222015-04-28 16:17:30 -0700711 brick_name: Brick locator to use. Overrides |board| if not None.
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700712 blueprint_name: Blueprint locator to use. Overrides |board| and
713 |brick_name|.
David Pursellf1d16a62015-03-25 13:31:04 -0700714 install: Install to USB using base disk layout; USB |device| scheme only.
715 src_image_to_delta: Local path to an image to be used as the base to
716 generate delta payloads; SSH |device| scheme only.
717 rootfs_update: Update rootfs partition; SSH |device| scheme only.
718 stateful_update: Update stateful partition; SSH |device| scheme only.
719 clobber_stateful: Clobber stateful partition; SSH |device| scheme only.
720 reboot: Reboot device after update; SSH |device| scheme only.
721 wipe: Wipe temporary working directory; SSH |device| scheme only.
722 ping: Ping the device before attempting update; SSH |device| scheme only.
723 disable_rootfs_verification: Remove rootfs verification after update; SSH
724 |device| scheme only.
725 clear_cache: Clear the devserver static directory.
726 yes: Assume "yes" for any prompt.
727 force: Ignore sanity checks and prompts. Overrides |yes| if True.
728 debug: Print additional debugging messages.
729
730 Raises:
731 FlashError: An unrecoverable error occured.
732 ValueError: Invalid parameter combination.
733 """
734 if force:
735 yes = True
736
737 if clear_cache:
738 logging.info('Clearing the cache...')
739 ds_wrapper.DevServerWrapper.WipeStaticDirectory(_DEVSERVER_STATIC_DIR)
740
741 try:
742 osutils.SafeMakedirsNonRoot(_DEVSERVER_STATIC_DIR)
743 except OSError:
744 logging.error('Failed to create %s', _DEVSERVER_STATIC_DIR)
745
746 if install:
David Pursell2e773382015-04-03 14:30:47 -0700747 if not device or device.scheme != commandline.DEVICE_SCHEME_USB:
David Pursellf1d16a62015-03-25 13:31:04 -0700748 raise ValueError(
749 '--install can only be used when writing to a USB device')
750 if not cros_build_lib.IsInsideChroot():
751 raise ValueError('--install can only be used inside the chroot')
752
753 # If installing an SDK image, find the version and override image path.
David Pursellf1d16a62015-03-25 13:31:04 -0700754 if project_sdk_image:
David Pursellf1d16a62015-03-25 13:31:04 -0700755 image = 'project_sdk'
Gilad Arnold94fc3b02015-04-29 13:04:32 -0700756 if sdk_version is None:
757 sdk_version = project_sdk.FindVersion()
758 if not sdk_version:
759 raise FlashError('Could not find SDK version')
David Pursellf1d16a62015-03-25 13:31:04 -0700760
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700761 # We don't have enough information on the device to make a good guess on
762 # whether this device is compatible with the blueprint.
763 # TODO(bsimonnet): Add proper compatibility checks. (brbug.com/969)
764 if blueprint_name:
765 board = None
766 if image == 'latest':
767 blueprint = blueprint_lib.Blueprint(blueprint_name)
768 image_dir = os.path.join(
769 workspace_lib.WorkspacePath(), workspace_lib.WORKSPACE_IMAGES_DIR,
770 blueprint.FriendlyName(), 'latest')
771 image = _ChooseImageFromDirectory(image_dir)
772 elif not os.path.exists(image):
773 raise ValueError('Cannot find blueprint image "%s". Only "latest" and '
774 'full image path are supported.' % image)
775 elif brick_name:
776 board = brick_lib.Brick(brick_name).FriendlyName()
David Pursellf1d16a62015-03-25 13:31:04 -0700777
David Pursell2e773382015-04-03 14:30:47 -0700778 if not device or device.scheme == commandline.DEVICE_SCHEME_SSH:
779 if device:
780 hostname, port = device.hostname, device.port
781 else:
782 hostname, port = None, None
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700783 logging.notice('Preparing to update the remote device %s', hostname)
David Pursellf1d16a62015-03-25 13:31:04 -0700784 updater = RemoteDeviceUpdater(
David Pursell2e773382015-04-03 14:30:47 -0700785 hostname,
786 port,
David Pursellf1d16a62015-03-25 13:31:04 -0700787 image,
788 board=board,
David Pursellf1d16a62015-03-25 13:31:04 -0700789 src_image_to_delta=src_image_to_delta,
790 rootfs_update=rootfs_update,
791 stateful_update=stateful_update,
792 clobber_stateful=clobber_stateful,
793 reboot=reboot,
794 wipe=wipe,
795 debug=debug,
796 yes=yes,
797 force=force,
798 ping=ping,
799 disable_verification=disable_rootfs_verification,
800 sdk_version=sdk_version)
801 updater.Run()
802 elif device.scheme == commandline.DEVICE_SCHEME_USB:
803 path = osutils.ExpandPath(device.path) if device.path else ''
804 logging.info('Preparing to image the removable device %s', path)
805 imager = USBImager(path,
806 board,
807 image,
808 sdk_version=sdk_version,
809 debug=debug,
810 install=install,
811 yes=yes)
812 imager.Run()
813 elif device.scheme == commandline.DEVICE_SCHEME_FILE:
814 logging.info('Preparing to copy image to %s', device.path)
815 imager = FileImager(device.path,
816 board,
817 image,
818 sdk_version=sdk_version,
819 debug=debug,
820 yes=yes)
821 imager.Run()