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