blob: 2d6449cc2bc36b3b85b6890ee38b6fe277e0f49d [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
Ralph Nathan9b997232015-05-15 13:13:12 -070011import re
David Pursellf1d16a62015-03-25 13:31:04 -070012import shutil
13import tempfile
14import time
15
16from chromite.cbuildbot import constants
Ralph Nathan872ea4d2015-05-05 18:04:56 -070017from chromite.cli import command
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -070018from chromite.lib import blueprint_lib
David Pursellf1d16a62015-03-25 13:31:04 -070019from chromite.lib import brick_lib
20from chromite.lib import commandline
21from chromite.lib import cros_build_lib
22from chromite.lib import cros_logging as logging
23from chromite.lib import dev_server_wrapper as ds_wrapper
Ralph Nathan872ea4d2015-05-05 18:04:56 -070024from chromite.lib import operation
David Pursellf1d16a62015-03-25 13:31:04 -070025from chromite.lib import osutils
Gilad Arnold1c8eda52015-05-04 22:32:38 -070026from chromite.lib import path_util
David Pursellf1d16a62015-03-25 13:31:04 -070027from chromite.lib import project_sdk
28from chromite.lib import remote_access
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -070029from chromite.lib import workspace_lib
David Pursellf1d16a62015-03-25 13:31:04 -070030
31
Gilad Arnold1c8eda52015-05-04 22:32:38 -070032_DEVSERVER_STATIC_DIR = path_util.FromChrootPath(
David Pursellf1d16a62015-03-25 13:31:04 -070033 os.path.join(constants.CHROOT_SOURCE_ROOT, 'devserver', 'static'))
34
35
Ralph Nathan9b997232015-05-15 13:13:12 -070036class UsbImagerOperation(operation.ProgressBarOperation):
37 """Progress bar for flashing image to operation."""
38
39 def __init__(self, image):
40 super(UsbImagerOperation, self).__init__()
41 self._size = os.path.getsize(image)
42 self._transferred = 0.
43 self._bytes = re.compile(r'(\d+) bytes')
44
45 def _GetDDPid(self):
46 """Get the Pid of dd."""
47 try:
48 pids = cros_build_lib.RunCommand(['pgrep', 'dd'], capture_output=True,
49 print_cmd=False).output
50 for pid in pids.splitlines():
51 if osutils.IsChildProcess(int(pid), name='dd'):
52 return int(pid)
53 return -1
54 except cros_build_lib.RunCommandError:
55 # If dd isn't still running, then we assume that it is finished.
56 return -1
57
58 def _PingDD(self, dd_pid):
59 """Send USR1 signal to dd to get status update."""
60 try:
61 cmd = ['kill', '-USR1', str(dd_pid)]
62 cros_build_lib.SudoRunCommand(cmd, print_cmd=False)
63 except cros_build_lib.RunCommandError:
64 # Here we assume that dd finished in the background.
65 return
66
67 def ParseOutput(self, output=None):
68 """Parse the output of dd to update progress bar."""
69 dd_pid = self._GetDDPid()
70 if dd_pid == -1:
71 return
72
73 self._PingDD(dd_pid)
74
75 if output is None:
76 stdout = self._stdout.read()
77 stderr = self._stderr.read()
78 output = stdout + stderr
79
80 match = self._bytes.search(output)
81 if match:
82 self._transferred = match.groups()[0]
83
84 self.ProgressBar(float(self._transferred) / self._size)
85
86
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -070087def _IsFilePathGPTDiskImage(file_path):
88 """Determines if a file is a valid GPT disk.
89
90 Args:
91 file_path: Path to the file to test.
92 """
93 if os.path.isfile(file_path):
94 with cros_build_lib.Open(file_path) as image_file:
95 image_file.seek(0x1fe)
96 if image_file.read(10) == '\x55\xaaEFI PART':
97 return True
98 return False
99
100
101def _ChooseImageFromDirectory(dir_path):
102 """Lists all image files in |dir_path| and ask user to select one.
103
104 Args:
105 dir_path: Path to the directory.
106 """
107 images = sorted([x for x in os.listdir(dir_path) if
108 _IsFilePathGPTDiskImage(os.path.join(dir_path, x))])
109 idx = 0
110 if len(images) == 0:
111 raise ValueError('No image found in %s.' % dir_path)
112 elif len(images) > 1:
113 idx = cros_build_lib.GetChoice(
114 'Multiple images found in %s. Please select one to continue:' % (
115 (dir_path,)),
116 images)
117
118 return os.path.join(dir_path, images[idx])
119
120
David Pursellf1d16a62015-03-25 13:31:04 -0700121class FlashError(Exception):
122 """Thrown when there is an unrecoverable error during flash."""
123
124
125class USBImager(object):
126 """Copy image to the target removable device."""
127
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700128 def __init__(self, device, board, image, workspace_path=None,
129 sdk_version=None, debug=False, install=False, yes=False):
David Pursellf1d16a62015-03-25 13:31:04 -0700130 """Initalizes USBImager."""
131 self.device = device
132 self.board = board if board else cros_build_lib.GetDefaultBoard()
133 self.image = image
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700134 self.workspace_path = workspace_path
David Pursellf1d16a62015-03-25 13:31:04 -0700135 self.sdk_version = sdk_version
136 self.debug = debug
137 self.debug_level = logging.DEBUG if debug else logging.INFO
138 self.install = install
139 self.yes = yes
140
141 def DeviceNameToPath(self, device_name):
142 return '/dev/%s' % device_name
143
144 def GetRemovableDeviceDescription(self, device):
145 """Returns a informational description of the removable |device|.
146
147 Args:
148 device: the device name (e.g. sdc).
149
150 Returns:
151 A string describing |device| (e.g. Patriot Memory 7918 MB).
152 """
153 desc = [
154 osutils.GetDeviceInfo(device, keyword='manufacturer'),
155 osutils.GetDeviceInfo(device, keyword='product'),
156 osutils.GetDeviceSize(self.DeviceNameToPath(device)),
157 '(%s)' % self.DeviceNameToPath(device),
158 ]
159 return ' '.join([x for x in desc if x])
160
161 def ListAllRemovableDevices(self):
162 """Returns a list of removable devices.
163
164 Returns:
165 A list of device names (e.g. ['sdb', 'sdc']).
166 """
167 devices = osutils.ListBlockDevices()
168 removable_devices = []
169 for d in devices:
170 if d.TYPE == 'disk' and d.RM == '1':
171 removable_devices.append(d.NAME)
172
173 return removable_devices
174
175 def ChooseRemovableDevice(self, devices):
176 """Lists all removable devices and asks user to select/confirm.
177
178 Args:
179 devices: a list of device names (e.g. ['sda', 'sdb']).
180
181 Returns:
182 The device name chosen by the user.
183 """
184 idx = cros_build_lib.GetChoice(
185 'Removable device(s) found. Please select/confirm to continue:',
186 [self.GetRemovableDeviceDescription(x) for x in devices])
187
188 return devices[idx]
189
190 def InstallImageToDevice(self, image, device):
191 """Installs |image| to the removable |device|.
192
193 Args:
194 image: Path to the image to copy.
195 device: Device to copy to.
196 """
197 cmd = [
198 'chromeos-install',
199 '--yes',
200 '--skip_src_removable',
201 '--skip_dst_removable',
202 '--payload_image=%s' % image,
203 '--dst=%s' % device,
204 '--skip_postinstall',
205 ]
206 cros_build_lib.SudoRunCommand(cmd)
207
208 def CopyImageToDevice(self, image, device):
209 """Copies |image| to the removable |device|.
210
211 Args:
212 image: Path to the image to copy.
213 device: Device to copy to.
214 """
Ralph Nathan9b997232015-05-15 13:13:12 -0700215 cmd = ['dd', 'if=%s' % image, 'of=%s' % device, 'bs=4M', 'iflag=fullblock',
216 'oflag=sync']
217 if logging.getLogger().getEffectiveLevel() <= logging.NOTICE:
218 op = UsbImagerOperation(image)
219 op.Run(cros_build_lib.SudoRunCommand, cmd, debug_level=logging.NOTICE,
220 update_period=0.5)
221 else:
222 cros_build_lib.SudoRunCommand(
223 cmd, debug_level=logging.NOTICE,
224 print_cmd=logging.getLogger().getEffectiveLevel() < logging.NOTICE)
David Pursellf1d16a62015-03-25 13:31:04 -0700225
David Pursellf1d16a62015-03-25 13:31:04 -0700226 cros_build_lib.SudoRunCommand(['sync'], debug_level=self.debug_level)
227
David Pursellf1d16a62015-03-25 13:31:04 -0700228 def _GetImagePath(self):
229 """Returns the image path to use."""
230 image_path = translated_path = None
231 if os.path.isfile(self.image):
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700232 if not self.yes and not _IsFilePathGPTDiskImage(self.image):
David Pursellf1d16a62015-03-25 13:31:04 -0700233 # TODO(wnwen): Open the tarball and if there is just one file in it,
234 # use that instead. Existing code in upload_symbols.py.
235 if cros_build_lib.BooleanPrompt(
236 prolog='The given image file is not a valid disk image. Perhaps '
237 'you forgot to untar it.',
238 prompt='Terminate the current flash process?'):
239 raise FlashError('Update terminated by user.')
240 image_path = self.image
241 elif os.path.isdir(self.image):
242 # Ask user which image (*.bin) in the folder to use.
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700243 image_path = _ChooseImageFromDirectory(self.image)
David Pursellf1d16a62015-03-25 13:31:04 -0700244 else:
245 # Translate the xbuddy path to get the exact image to use.
Gilad Arnolde62ec902015-04-24 14:41:02 -0700246 translated_path, _ = ds_wrapper.GetImagePathWithXbuddy(
David Pursellf1d16a62015-03-25 13:31:04 -0700247 self.image, self.board, version=self.sdk_version,
248 static_dir=_DEVSERVER_STATIC_DIR)
249 image_path = ds_wrapper.TranslatedPathToLocalPath(
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700250 translated_path, _DEVSERVER_STATIC_DIR,
251 workspace_path=self.workspace_path)
David Pursellf1d16a62015-03-25 13:31:04 -0700252
253 logging.info('Using image %s', translated_path or image_path)
254 return image_path
255
256 def Run(self):
257 """Image the removable device."""
258 devices = self.ListAllRemovableDevices()
259
260 if self.device:
261 # If user specified a device path, check if it exists.
262 if not os.path.exists(self.device):
263 raise FlashError('Device path %s does not exist.' % self.device)
264
265 # Then check if it is removable.
266 if self.device not in [self.DeviceNameToPath(x) for x in devices]:
267 msg = '%s is not a removable device.' % self.device
268 if not (self.yes or cros_build_lib.BooleanPrompt(
269 default=False, prolog=msg)):
270 raise FlashError('You can specify usb:// to choose from a list of '
271 'removable devices.')
272 target = None
273 if self.device:
274 # Get device name from path (e.g. sdc in /dev/sdc).
275 target = self.device.rsplit(os.path.sep, 1)[-1]
276 elif devices:
277 # Ask user to choose from the list.
278 target = self.ChooseRemovableDevice(devices)
279 else:
280 raise FlashError('No removable devices detected.')
281
282 image_path = self._GetImagePath()
283 try:
284 device = self.DeviceNameToPath(target)
285 if self.install:
286 self.InstallImageToDevice(image_path, device)
287 else:
288 self.CopyImageToDevice(image_path, device)
289 except cros_build_lib.RunCommandError:
290 logging.error('Failed copying image to device %s',
291 self.DeviceNameToPath(target))
292
293
294class FileImager(USBImager):
295 """Copy image to the target path."""
296
297 def Run(self):
298 """Copy the image to the path specified by self.device."""
299 if not os.path.exists(self.device):
300 raise FlashError('Path %s does not exist.' % self.device)
301
302 image_path = self._GetImagePath()
303 if os.path.isdir(self.device):
304 logging.info('Copying to %s',
305 os.path.join(self.device, os.path.basename(image_path)))
306 else:
307 logging.info('Copying to %s', self.device)
308 try:
309 shutil.copy(image_path, self.device)
310 except IOError:
311 logging.error('Failed to copy image %s to %s', image_path, self.device)
312
313
314class RemoteDeviceUpdater(object):
315 """Performs update on a remote device."""
316 DEVSERVER_FILENAME = 'devserver.py'
317 STATEFUL_UPDATE_BIN = '/usr/bin/stateful_update'
318 UPDATE_ENGINE_BIN = 'update_engine_client'
David Pursellf1d16a62015-03-25 13:31:04 -0700319 # Root working directory on the device. This directory is in the
320 # stateful partition and thus has enough space to store the payloads.
321 DEVICE_BASE_DIR = '/mnt/stateful_partition/cros-flash'
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700322 UPDATE_CHECK_INTERVAL_PROGRESSBAR = 0.5
323 UPDATE_CHECK_INTERVAL_NORMAL = 10
David Pursellf1d16a62015-03-25 13:31:04 -0700324
325 def __init__(self, ssh_hostname, ssh_port, image, stateful_update=True,
326 rootfs_update=True, clobber_stateful=False, reboot=True,
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700327 board=None, workspace_path=None, src_image_to_delta=None,
328 wipe=True, debug=False, yes=False, force=False, ping=True,
329 disable_verification=False, sdk_version=None):
David Pursellf1d16a62015-03-25 13:31:04 -0700330 """Initializes RemoteDeviceUpdater"""
331 if not stateful_update and not rootfs_update:
332 raise ValueError('No update operation to perform; either stateful or'
333 ' rootfs partitions must be updated.')
334 self.tempdir = tempfile.mkdtemp(prefix='cros-flash')
335 self.ssh_hostname = ssh_hostname
336 self.ssh_port = ssh_port
337 self.image = image
338 self.board = board
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700339 self.workspace_path = workspace_path
David Pursellf1d16a62015-03-25 13:31:04 -0700340 self.src_image_to_delta = src_image_to_delta
341 self.do_stateful_update = stateful_update
342 self.do_rootfs_update = rootfs_update
343 self.disable_verification = disable_verification
344 self.clobber_stateful = clobber_stateful
345 self.reboot = reboot
346 self.debug = debug
347 self.ping = ping
348 # Do not wipe if debug is set.
349 self.wipe = wipe and not debug
350 self.yes = yes
351 self.force = force
352 self.sdk_version = sdk_version
353
354 # pylint: disable=unbalanced-tuple-unpacking
355 @classmethod
356 def GetUpdateStatus(cls, device, keys=None):
357 """Returns the status of the update engine on the |device|.
358
359 Retrieves the status from update engine and confirms all keys are
360 in the status.
361
362 Args:
363 device: A ChromiumOSDevice object.
364 keys: the keys to look for in the status result (defaults to
365 ['CURRENT_OP']).
366
367 Returns:
368 A list of values in the order of |keys|.
369 """
370 keys = ['CURRENT_OP'] if not keys else keys
371 result = device.RunCommand([cls.UPDATE_ENGINE_BIN, '--status'],
372 capture_output=True)
373 if not result.output:
374 raise Exception('Cannot get update status')
375
376 try:
377 status = cros_build_lib.LoadKeyValueFile(
378 cStringIO.StringIO(result.output))
379 except ValueError:
380 raise ValueError('Cannot parse update status')
381
382 values = []
383 for key in keys:
384 if key not in status:
385 raise ValueError('Missing %s in the update engine status')
386
387 values.append(status.get(key))
388
389 return values
390
391 def UpdateStateful(self, device, payload, clobber=False):
392 """Update the stateful partition of the device.
393
394 Args:
395 device: The ChromiumOSDevice object to update.
396 payload: The path to the update payload.
397 clobber: Clobber stateful partition (defaults to False).
398 """
399 # Copy latest stateful_update to device.
Gilad Arnold1c8eda52015-05-04 22:32:38 -0700400 stateful_update_bin = path_util.FromChrootPath(
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700401 self.STATEFUL_UPDATE_BIN, workspace_path=self.workspace_path)
David Pursellf1d16a62015-03-25 13:31:04 -0700402 device.CopyToWorkDir(stateful_update_bin)
403 msg = 'Updating stateful partition'
404 logging.info('Copying stateful payload to device...')
405 device.CopyToWorkDir(payload)
406 cmd = ['sh',
407 os.path.join(device.work_dir,
408 os.path.basename(self.STATEFUL_UPDATE_BIN)),
409 os.path.join(device.work_dir, os.path.basename(payload))]
410
411 if clobber:
412 cmd.append('--stateful_change=clean')
413 msg += ' with clobber enabled'
414
415 logging.info('%s...', msg)
416 try:
417 device.RunCommand(cmd)
418 except cros_build_lib.RunCommandError:
419 logging.error('Faild to perform stateful partition update.')
420
421 def _CopyDevServerPackage(self, device, tempdir):
422 """Copy devserver package to work directory of device.
423
424 Args:
425 device: The ChromiumOSDevice object to copy the package to.
426 tempdir: The directory to temporarily store devserver package.
427 """
428 logging.info('Copying devserver package to device...')
429 src_dir = os.path.join(tempdir, 'src')
430 osutils.RmDir(src_dir, ignore_missing=True)
431 shutil.copytree(
432 ds_wrapper.DEVSERVER_PKG_DIR, src_dir,
433 ignore=shutil.ignore_patterns('*.pyc', 'tmp*', '.*', 'static', '*~'))
434 device.CopyToWorkDir(src_dir)
435 return os.path.join(device.work_dir, os.path.basename(src_dir))
436
437 def SetupRootfsUpdate(self, device):
438 """Makes sure |device| is ready for rootfs update."""
439 logging.info('Checking if update engine is idle...')
440 status, = self.GetUpdateStatus(device)
441 if status == 'UPDATE_STATUS_UPDATED_NEED_REBOOT':
442 logging.info('Device needs to reboot before updating...')
443 device.Reboot()
444 status, = self.GetUpdateStatus(device)
445
446 if status != 'UPDATE_STATUS_IDLE':
447 raise FlashError('Update engine is not idle. Status: %s' % status)
448
449 def UpdateRootfs(self, device, payload, tempdir):
450 """Update the rootfs partition of the device.
451
452 Args:
453 device: The ChromiumOSDevice object to update.
454 payload: The path to the update payload.
455 tempdir: The directory to store temporary files.
456 """
457 # Setup devserver and payload on the target device.
458 static_dir = os.path.join(device.work_dir, 'static')
459 payload_dir = os.path.join(static_dir, 'pregenerated')
460 src_dir = self._CopyDevServerPackage(device, tempdir)
461 device.RunCommand(['mkdir', '-p', payload_dir])
462 logging.info('Copying rootfs payload to device...')
463 device.CopyToDevice(payload, payload_dir)
464 devserver_bin = os.path.join(src_dir, self.DEVSERVER_FILENAME)
465 ds = ds_wrapper.RemoteDevServerWrapper(
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700466 device, devserver_bin, workspace_path=self.workspace_path,
467 static_dir=static_dir, log_dir=device.work_dir)
David Pursellf1d16a62015-03-25 13:31:04 -0700468
469 logging.info('Updating rootfs partition')
470 try:
471 ds.Start()
472 # Use the localhost IP address to ensure that update engine
473 # client can connect to the devserver.
474 omaha_url = ds.GetDevServerURL(
475 ip='127.0.0.1', port=ds.port, sub_dir='update/pregenerated')
476 cmd = [self.UPDATE_ENGINE_BIN, '-check_for_update',
477 '-omaha_url=%s' % omaha_url]
478 device.RunCommand(cmd)
479
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700480 # If we are using a progress bar, update it every 0.5s instead of 10s.
481 if command.UseProgressBar():
482 update_check_interval = self.UPDATE_CHECK_INTERVAL_PROGRESSBAR
483 oper = operation.ProgressBarOperation()
484 else:
485 update_check_interval = self.UPDATE_CHECK_INTERVAL_NORMAL
486 oper = None
487 end_message_not_printed = True
488
David Pursellf1d16a62015-03-25 13:31:04 -0700489 # Loop until update is complete.
490 while True:
491 op, progress = self.GetUpdateStatus(device, ['CURRENT_OP', 'PROGRESS'])
492 logging.info('Waiting for update...status: %s at progress %s',
493 op, progress)
494
495 if op == 'UPDATE_STATUS_UPDATED_NEED_REBOOT':
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700496 logging.notice('Update completed.')
David Pursellf1d16a62015-03-25 13:31:04 -0700497 break
498
499 if op == 'UPDATE_STATUS_IDLE':
500 raise FlashError(
501 'Update failed with unexpected update status: %s' % op)
502
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700503 if oper is not None:
504 if op == 'UPDATE_STATUS_DOWNLOADING':
505 oper.ProgressBar(float(progress))
506 elif end_message_not_printed and op == 'UPDATE_STATUS_FINALIZING':
507 oper.Cleanup()
508 logging.notice('Finalizing image.')
509 end_message_not_printed = False
510
511 time.sleep(update_check_interval)
David Pursellf1d16a62015-03-25 13:31:04 -0700512
513 ds.Stop()
514 except Exception:
515 logging.error('Rootfs update failed.')
516 logging.warning(ds.TailLog() or 'No devserver log is available.')
517 raise
518 finally:
519 ds.Stop()
520 device.CopyFromDevice(ds.log_file,
521 os.path.join(tempdir, 'target_devserver.log'),
522 error_code_ok=True)
523 device.CopyFromDevice('/var/log/update_engine.log', tempdir,
524 follow_symlinks=True,
525 error_code_ok=True)
526
527 def _CheckPayloads(self, payload_dir):
528 """Checks that all update payloads exists in |payload_dir|."""
529 filenames = []
530 filenames += [ds_wrapper.ROOTFS_FILENAME] if self.do_rootfs_update else []
531 if self.do_stateful_update:
532 filenames += [ds_wrapper.STATEFUL_FILENAME]
533 for fname in filenames:
534 payload = os.path.join(payload_dir, fname)
535 if not os.path.exists(payload):
536 raise FlashError('Payload %s does not exist!' % payload)
537
538 def Verify(self, old_root_dev, new_root_dev):
539 """Verifies that the root deivce changed after reboot."""
540 assert new_root_dev and old_root_dev
541 if new_root_dev == old_root_dev:
542 raise FlashError(
543 'Failed to boot into the new version. Possibly there was a '
544 'signing problem, or an automated rollback occurred because '
545 'your new image failed to boot.')
546
547 @classmethod
548 def GetRootDev(cls, device):
549 """Get the current root device on |device|."""
550 rootdev = device.RunCommand(
551 ['rootdev', '-s'], capture_output=True).output.strip()
552 logging.debug('Current root device is %s', rootdev)
553 return rootdev
554
555 def Cleanup(self):
556 """Cleans up the temporary directory."""
557 if self.wipe:
558 logging.info('Cleaning up temporary working directory...')
559 osutils.RmDir(self.tempdir)
560 else:
561 logging.info('You can find the log files and/or payloads in %s',
562 self.tempdir)
563
564 def _CanRunDevserver(self, device, tempdir):
565 """We can run devserver on |device|.
566
567 If the stateful partition is corrupted, Python or other packages
568 (e.g. cherrypy) needed for rootfs update may be missing on |device|.
569
570 This will also use `ldconfig` to update library paths on the target
571 device if it looks like that's causing problems, which is necessary
572 for base images.
573
574 Args:
Mike Frysinger6f3c48e2015-05-06 02:38:51 -0400575 device: A ChromiumOSDevice object.
576 tempdir: A temporary directory to store files.
David Pursellf1d16a62015-03-25 13:31:04 -0700577
578 Returns:
579 True if we can start devserver; False otherwise.
580 """
581 logging.info('Checking if we can run devserver on the device.')
582 src_dir = self._CopyDevServerPackage(device, tempdir)
583 devserver_bin = os.path.join(src_dir, self.DEVSERVER_FILENAME)
584 devserver_check_command = ['python', devserver_bin, '--help']
585 try:
586 device.RunCommand(devserver_check_command)
587 except cros_build_lib.RunCommandError as e:
588 logging.warning('Cannot start devserver: %s', e)
589 if 'python: error while loading shared libraries' in str(e):
590 logging.info('Attempting to correct device library paths...')
591 try:
592 device.RunCommand(['ldconfig', '-r', '/'])
593 device.RunCommand(devserver_check_command)
594 logging.info('Library path correction successful.')
595 return True
596 except cros_build_lib.RunCommandError as e2:
597 logging.warning('Library path correction failed: %s', e2)
598
599 return False
600
601 return True
602
603 def Run(self):
604 """Performs remote device update."""
605 old_root_dev, new_root_dev = None, None
606 try:
607 device_connected = False
608 with remote_access.ChromiumOSDeviceHandler(
609 self.ssh_hostname, port=self.ssh_port,
610 base_dir=self.DEVICE_BASE_DIR, ping=self.ping) as device:
611 device_connected = True
612
David Pursellf1d16a62015-03-25 13:31:04 -0700613 payload_dir = self.tempdir
614 if os.path.isdir(self.image):
615 # If the given path is a directory, we use the provided
616 # update payload(s) in the directory.
617 payload_dir = self.image
618 logging.info('Using provided payloads in %s', payload_dir)
619 else:
620 if os.path.isfile(self.image):
621 # If the given path is an image, make sure devserver can
622 # access it and generate payloads.
623 logging.info('Using image %s', self.image)
624 ds_wrapper.GetUpdatePayloadsFromLocalPath(
625 self.image, payload_dir,
626 src_image_to_delta=self.src_image_to_delta,
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700627 static_dir=_DEVSERVER_STATIC_DIR,
628 workspace_path=self.workspace_path)
David Pursellf1d16a62015-03-25 13:31:04 -0700629 else:
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700630 # We should ignore the given/inferred board value and stick to the
631 # device's basic designation. We do emit a warning for good
632 # measure.
633 # TODO(garnold) In fact we should find the board/overlay that the
634 # device inherits from and which defines the SDK "baseline" image
635 # (brillo:339).
636 if self.sdk_version and self.board and not self.force:
637 logging.warning(
638 'Ignoring board value (%s) and deferring to device; use '
639 '--force to override',
640 self.board)
641 self.board = None
642
643 self.board = cros_build_lib.GetBoard(device_board=device.board,
644 override_board=self.board,
645 force=self.yes)
646 if not self.board:
647 raise FlashError('No board identified')
648
649 if not self.force and self.board != device.board:
650 # If a board was specified, it must be compatible with the device.
651 raise FlashError('Device (%s) is incompatible with board %s',
652 device.board, self.board)
653
654 logging.info('Board is %s', self.board)
655
David Pursellf1d16a62015-03-25 13:31:04 -0700656 # Translate the xbuddy path to get the exact image to use.
Gilad Arnolde62ec902015-04-24 14:41:02 -0700657 translated_path, resolved_path = ds_wrapper.GetImagePathWithXbuddy(
David Pursellf1d16a62015-03-25 13:31:04 -0700658 self.image, self.board, version=self.sdk_version,
659 static_dir=_DEVSERVER_STATIC_DIR, lookup_only=True)
660 logging.info('Using image %s', translated_path)
661 # Convert the translated path to be used in the update request.
Gilad Arnolde62ec902015-04-24 14:41:02 -0700662 image_path = ds_wrapper.ConvertTranslatedPath(resolved_path,
David Pursellf1d16a62015-03-25 13:31:04 -0700663 translated_path)
664
665 # Launch a local devserver to generate/serve update payloads.
666 ds_wrapper.GetUpdatePayloads(
667 image_path, payload_dir, board=self.board,
668 src_image_to_delta=self.src_image_to_delta,
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700669 workspace_path=self.workspace_path,
David Pursellf1d16a62015-03-25 13:31:04 -0700670 static_dir=_DEVSERVER_STATIC_DIR)
671
672 # Verify that all required payloads are in the payload directory.
673 self._CheckPayloads(payload_dir)
674
675 restore_stateful = False
676 if (not self._CanRunDevserver(device, self.tempdir) and
677 self.do_rootfs_update):
678 msg = ('Cannot start devserver! The stateful partition may be '
679 'corrupted.')
680 prompt = 'Attempt to restore the stateful partition?'
681 restore_stateful = self.yes or cros_build_lib.BooleanPrompt(
682 prompt=prompt, default=False, prolog=msg)
683 if not restore_stateful:
684 raise FlashError('Cannot continue to perform rootfs update!')
685
686 if restore_stateful:
687 logging.warning('Restoring the stateful partition...')
688 payload = os.path.join(payload_dir, ds_wrapper.STATEFUL_FILENAME)
689 self.UpdateStateful(device, payload, clobber=self.clobber_stateful)
690 device.Reboot()
691 if self._CanRunDevserver(device, self.tempdir):
692 logging.info('Stateful partition restored.')
693 else:
694 raise FlashError('Unable to restore stateful partition.')
695
696 # Perform device updates.
697 if self.do_rootfs_update:
698 self.SetupRootfsUpdate(device)
699 # Record the current root device. This must be done after
700 # SetupRootfsUpdate because SetupRootfsUpdate may reboot the
701 # device if there is a pending update, which changes the
702 # root device.
703 old_root_dev = self.GetRootDev(device)
704 payload = os.path.join(payload_dir, ds_wrapper.ROOTFS_FILENAME)
705 self.UpdateRootfs(device, payload, self.tempdir)
706 logging.info('Rootfs update completed.')
707
708 if self.do_stateful_update and not restore_stateful:
709 payload = os.path.join(payload_dir, ds_wrapper.STATEFUL_FILENAME)
710 self.UpdateStateful(device, payload, clobber=self.clobber_stateful)
711 logging.info('Stateful update completed.')
712
713 if self.reboot:
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700714 logging.notice('Rebooting device...')
David Pursellf1d16a62015-03-25 13:31:04 -0700715 device.Reboot()
716 if self.clobber_stateful:
717 # --clobber-stateful wipes the stateful partition and the
718 # working directory on the device no longer exists. To
719 # remedy this, we recreate the working directory here.
720 device.BaseRunCommand(['mkdir', '-p', device.work_dir])
721
722 if self.do_rootfs_update and self.reboot:
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700723 logging.notice('Verifying that the device has been updated...')
David Pursellf1d16a62015-03-25 13:31:04 -0700724 new_root_dev = self.GetRootDev(device)
725 self.Verify(old_root_dev, new_root_dev)
726
727 if self.disable_verification:
728 logging.info('Disabling rootfs verification on the device...')
729 device.DisableRootfsVerification()
730
731 except Exception:
732 logging.error('Device update failed.')
733 if device_connected and device.lsb_release:
734 lsb_entries = sorted(device.lsb_release.items())
735 logging.info('Following are the LSB version details of the device:\n%s',
736 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
737 raise
738 else:
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700739 logging.notice('Update performed successfully.')
David Pursellf1d16a62015-03-25 13:31:04 -0700740 finally:
741 self.Cleanup()
742
743
744# TODO(dpursell): replace |brick| argument with blueprints when they're ready.
Gilad Arnold94fc3b02015-04-29 13:04:32 -0700745def Flash(device, image, project_sdk_image=False, sdk_version=None, board=None,
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700746 brick_name=None, blueprint_name=None, install=False,
747 src_image_to_delta=None, rootfs_update=True, stateful_update=True,
748 clobber_stateful=False, reboot=True, wipe=True, ping=True,
749 disable_rootfs_verification=False, clear_cache=False, yes=False,
750 force=False, debug=False):
David Pursellf1d16a62015-03-25 13:31:04 -0700751 """Flashes a device, USB drive, or file with an image.
752
753 This provides functionality common to `cros flash` and `brillo flash`
754 so that they can parse the commandline separately but still use the
755 same underlying functionality.
756
757 Args:
David Pursell2e773382015-04-03 14:30:47 -0700758 device: commandline.Device object; None to use the default device.
David Pursellf1d16a62015-03-25 13:31:04 -0700759 image: Path (string) to the update image. Can be a local or xbuddy path;
760 non-existant local paths are converted to xbuddy.
761 project_sdk_image: Use a clean project SDK image. Overrides |image| if True.
Gilad Arnold94fc3b02015-04-29 13:04:32 -0700762 sdk_version: Which version of SDK image to flash; autodetected if None.
David Pursellf1d16a62015-03-25 13:31:04 -0700763 board: Board to use; None to automatically detect.
Gilad Arnold23bfb222015-04-28 16:17:30 -0700764 brick_name: Brick locator to use. Overrides |board| if not None.
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700765 blueprint_name: Blueprint locator to use. Overrides |board| and
766 |brick_name|.
David Pursellf1d16a62015-03-25 13:31:04 -0700767 install: Install to USB using base disk layout; USB |device| scheme only.
768 src_image_to_delta: Local path to an image to be used as the base to
769 generate delta payloads; SSH |device| scheme only.
770 rootfs_update: Update rootfs partition; SSH |device| scheme only.
771 stateful_update: Update stateful partition; SSH |device| scheme only.
772 clobber_stateful: Clobber stateful partition; SSH |device| scheme only.
773 reboot: Reboot device after update; SSH |device| scheme only.
774 wipe: Wipe temporary working directory; SSH |device| scheme only.
775 ping: Ping the device before attempting update; SSH |device| scheme only.
776 disable_rootfs_verification: Remove rootfs verification after update; SSH
777 |device| scheme only.
778 clear_cache: Clear the devserver static directory.
779 yes: Assume "yes" for any prompt.
780 force: Ignore sanity checks and prompts. Overrides |yes| if True.
781 debug: Print additional debugging messages.
782
783 Raises:
784 FlashError: An unrecoverable error occured.
785 ValueError: Invalid parameter combination.
786 """
787 if force:
788 yes = True
789
790 if clear_cache:
791 logging.info('Clearing the cache...')
792 ds_wrapper.DevServerWrapper.WipeStaticDirectory(_DEVSERVER_STATIC_DIR)
793
794 try:
795 osutils.SafeMakedirsNonRoot(_DEVSERVER_STATIC_DIR)
796 except OSError:
797 logging.error('Failed to create %s', _DEVSERVER_STATIC_DIR)
798
799 if install:
David Pursell2e773382015-04-03 14:30:47 -0700800 if not device or device.scheme != commandline.DEVICE_SCHEME_USB:
David Pursellf1d16a62015-03-25 13:31:04 -0700801 raise ValueError(
802 '--install can only be used when writing to a USB device')
803 if not cros_build_lib.IsInsideChroot():
804 raise ValueError('--install can only be used inside the chroot')
805
806 # If installing an SDK image, find the version and override image path.
David Pursellf1d16a62015-03-25 13:31:04 -0700807 if project_sdk_image:
David Pursellf1d16a62015-03-25 13:31:04 -0700808 image = 'project_sdk'
Gilad Arnold94fc3b02015-04-29 13:04:32 -0700809 if sdk_version is None:
810 sdk_version = project_sdk.FindVersion()
811 if not sdk_version:
812 raise FlashError('Could not find SDK version')
David Pursellf1d16a62015-03-25 13:31:04 -0700813
Bertrand SIMONNET56f773d2015-05-04 14:02:39 -0700814 # We don't have enough information on the device to make a good guess on
815 # whether this device is compatible with the blueprint.
816 # TODO(bsimonnet): Add proper compatibility checks. (brbug.com/969)
817 if blueprint_name:
818 board = None
819 if image == 'latest':
820 blueprint = blueprint_lib.Blueprint(blueprint_name)
821 image_dir = os.path.join(
822 workspace_lib.WorkspacePath(), workspace_lib.WORKSPACE_IMAGES_DIR,
823 blueprint.FriendlyName(), 'latest')
824 image = _ChooseImageFromDirectory(image_dir)
825 elif not os.path.exists(image):
826 raise ValueError('Cannot find blueprint image "%s". Only "latest" and '
827 'full image path are supported.' % image)
828 elif brick_name:
829 board = brick_lib.Brick(brick_name).FriendlyName()
David Pursellf1d16a62015-03-25 13:31:04 -0700830
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700831 workspace_path = workspace_lib.WorkspacePath()
832
David Pursell2e773382015-04-03 14:30:47 -0700833 if not device or device.scheme == commandline.DEVICE_SCHEME_SSH:
834 if device:
835 hostname, port = device.hostname, device.port
836 else:
837 hostname, port = None, None
Ralph Nathan872ea4d2015-05-05 18:04:56 -0700838 logging.notice('Preparing to update the remote device %s', hostname)
David Pursellf1d16a62015-03-25 13:31:04 -0700839 updater = RemoteDeviceUpdater(
David Pursell2e773382015-04-03 14:30:47 -0700840 hostname,
841 port,
David Pursellf1d16a62015-03-25 13:31:04 -0700842 image,
843 board=board,
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700844 workspace_path=workspace_path,
David Pursellf1d16a62015-03-25 13:31:04 -0700845 src_image_to_delta=src_image_to_delta,
846 rootfs_update=rootfs_update,
847 stateful_update=stateful_update,
848 clobber_stateful=clobber_stateful,
849 reboot=reboot,
850 wipe=wipe,
851 debug=debug,
852 yes=yes,
853 force=force,
854 ping=ping,
855 disable_verification=disable_rootfs_verification,
856 sdk_version=sdk_version)
857 updater.Run()
858 elif device.scheme == commandline.DEVICE_SCHEME_USB:
859 path = osutils.ExpandPath(device.path) if device.path else ''
860 logging.info('Preparing to image the removable device %s', path)
861 imager = USBImager(path,
862 board,
863 image,
Gilad Arnold8ecd42a2015-04-27 11:47:05 -0700864 workspace_path=workspace_path,
David Pursellf1d16a62015-03-25 13:31:04 -0700865 sdk_version=sdk_version,
866 debug=debug,
867 install=install,
868 yes=yes)
869 imager.Run()
870 elif device.scheme == commandline.DEVICE_SCHEME_FILE:
871 logging.info('Preparing to copy image to %s', device.path)
872 imager = FileImager(device.path,
873 board,
874 image,
875 sdk_version=sdk_version,
876 debug=debug,
877 yes=yes)
878 imager.Run()