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