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