blob: 0d6cac454cfa5ac1672da74a921dcb7acbd607fc [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
David Pursell9476bf42015-03-30 13:34:27 -07002# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Deploy packages onto a target device."""
7
8from __future__ import print_function
9
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070010import bz2
David Pursell9476bf42015-03-30 13:34:27 -070011import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070012import functools
David Pursell9476bf42015-03-30 13:34:27 -070013import json
14import os
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070015import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070016
Ralph Nathane01ccf12015-04-16 10:40:32 -070017from chromite.cli import command
David Pursell9476bf42015-03-30 13:34:27 -070018from chromite.lib import cros_build_lib
19from chromite.lib import cros_logging as logging
Ralph Nathane01ccf12015-04-16 10:40:32 -070020from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070021from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070022from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070023from chromite.lib import remote_access
24try:
25 import portage
26except ImportError:
27 if cros_build_lib.IsInsideChroot():
28 raise
29
30
31_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
32# This is defined in src/platform/dev/builder.py
33_STRIPPED_PACKAGES_DIR = 'stripped-packages'
34
35_MAX_UPDATES_NUM = 10
36_MAX_UPDATES_WARNING = (
37 'You are about to update a large number of installed packages, which '
38 'might take a long time, fail midway, or leave the target in an '
39 'inconsistent state. It is highly recommended that you flash a new image '
40 'instead.')
41
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070042_DLC_ID = 'DLC_ID'
43_DLC_PACKAGE = 'DLC_PACKAGE'
44_ENVIRONMENT_FILENAME = 'environment.bz2'
45_DLC_INSTALL_ROOT = '/var/cache/dlc'
46
David Pursell9476bf42015-03-30 13:34:27 -070047
48class DeployError(Exception):
49 """Thrown when an unrecoverable error is encountered during deploy."""
50
51
Ralph Nathane01ccf12015-04-16 10:40:32 -070052class BrilloDeployOperation(operation.ProgressBarOperation):
53 """ProgressBarOperation specific for brillo deploy."""
Ralph Nathan90475a12015-05-20 13:19:01 -070054 MERGE_EVENTS = ['NOTICE: Copying', 'NOTICE: Installing',
Achuith Bhandarkar0487c312019-04-22 12:19:25 -070055 'emerge --usepkg', 'has been installed.']
Ralph Nathan90475a12015-05-20 13:19:01 -070056 UNMERGE_EVENTS = ['NOTICE: Unmerging', 'has been uninstalled.']
Ralph Nathane01ccf12015-04-16 10:40:32 -070057
58 def __init__(self, pkg_count, emerge):
59 """Construct BrilloDeployOperation object.
60
61 Args:
62 pkg_count: number of packages being built.
63 emerge: True if emerge, False is unmerge.
64 """
65 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070066 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070067 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070068 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070069 self._events = self.UNMERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070070 self._total = pkg_count * len(self._events)
71 self._completed = 0
72
Ralph Nathandc14ed92015-04-22 11:17:40 -070073 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070074 """Parse the output of brillo deploy to update a progress bar."""
75 stdout = self._stdout.read()
76 stderr = self._stderr.read()
77 output = stdout + stderr
78 for event in self._events:
79 self._completed += output.count(event)
80 self.ProgressBar(float(self._completed) / self._total)
81
82
David Pursell9476bf42015-03-30 13:34:27 -070083class _InstallPackageScanner(object):
84 """Finds packages that need to be installed on a target device.
85
86 Scans the sysroot bintree, beginning with a user-provided list of packages,
87 to find all packages that need to be installed. If so instructed,
88 transitively scans forward (mandatory) and backward (optional) dependencies
89 as well. A package will be installed if missing on the target (mandatory
90 packages only), or it will be updated if its sysroot version and build time
91 are different from the target. Common usage:
92
93 pkg_scanner = _InstallPackageScanner(sysroot)
94 pkgs = pkg_scanner.Run(...)
95 """
96
97 class VartreeError(Exception):
98 """An error in the processing of the installed packages tree."""
99
100 class BintreeError(Exception):
101 """An error in the processing of the source binpkgs tree."""
102
103 class PkgInfo(object):
104 """A record containing package information."""
105
106 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
107
108 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
109 self.cpv = cpv
110 self.build_time = build_time
111 self.rdeps_raw = rdeps_raw
112 self.rdeps = set() if rdeps is None else rdeps
113 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
114
115 # Python snippet for dumping vartree info on the target. Instantiate using
116 # _GetVartreeSnippet().
117 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700118import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700119import os
120import portage
121
122# Normalize the path to match what portage will index.
123target_root = os.path.normpath('%(root)s')
124if not target_root.endswith('/'):
125 target_root += '/'
126trees = portage.create_trees(target_root=target_root, config_root='/')
127vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700128pkg_info = []
129for cpv in vartree.dbapi.cpv_all():
130 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
131 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
132 pkg_info.append((cpv, slot, rdep_raw, build_time))
133
134print(json.dumps(pkg_info))
135"""
136
137 def __init__(self, sysroot):
138 self.sysroot = sysroot
139 # Members containing the sysroot (binpkg) and target (installed) package DB.
140 self.target_db = None
141 self.binpkgs_db = None
142 # Members for managing the dependency resolution work queue.
143 self.queue = None
144 self.seen = None
145 self.listed = None
146
147 @staticmethod
148 def _GetCP(cpv):
149 """Returns the CP value for a given CPV string."""
150 attrs = portage_util.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600151 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700152 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600153 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700154
155 @staticmethod
156 def _InDB(cp, slot, db):
157 """Returns whether CP and slot are found in a database (if provided)."""
158 cp_slots = db.get(cp) if db else None
159 return cp_slots is not None and (not slot or slot in cp_slots)
160
161 @staticmethod
162 def _AtomStr(cp, slot):
163 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
164 return '%s:%s' % (cp, slot) if slot else cp
165
166 @classmethod
167 def _GetVartreeSnippet(cls, root='/'):
168 """Returns a code snippet for dumping the vartree on the target.
169
170 Args:
171 root: The installation root.
172
173 Returns:
174 The said code snippet (string) with parameters filled in.
175 """
176 return cls._GET_VARTREE % {'root': root}
177
178 @classmethod
179 def _StripDepAtom(cls, dep_atom, installed_db=None):
180 """Strips a dependency atom and returns a (CP, slot) pair."""
181 # TODO(garnold) This is a gross simplification of ebuild dependency
182 # semantics, stripping and ignoring various qualifiers (versions, slots,
183 # USE flag, negation) and will likely need to be fixed. chromium:447366.
184
185 # Ignore unversioned blockers, leaving them for the user to resolve.
186 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
187 return None, None
188
189 cp = dep_atom
190 slot = None
191 require_installed = False
192
193 # Versioned blockers should be updated, but only if already installed.
194 # These are often used for forcing cascaded updates of multiple packages,
195 # so we're treating them as ordinary constraints with hopes that it'll lead
196 # to the desired result.
197 if cp.startswith('!'):
198 cp = cp.lstrip('!')
199 require_installed = True
200
201 # Remove USE flags.
202 if '[' in cp:
203 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
204
205 # Separate the slot qualifier and strip off subslots.
206 if ':' in cp:
207 cp, slot = cp.split(':')
208 for delim in ('/', '='):
209 slot = slot.split(delim, 1)[0]
210
211 # Strip version wildcards (right), comparators (left).
212 cp = cp.rstrip('*')
213 cp = cp.lstrip('<=>~')
214
215 # Turn into CP form.
216 cp = cls._GetCP(cp)
217
218 if require_installed and not cls._InDB(cp, None, installed_db):
219 return None, None
220
221 return cp, slot
222
223 @classmethod
224 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
225 """Resolves and returns a list of dependencies from a dependency string.
226
227 This parses a dependency string and returns a list of package names and
228 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
229 resolving disjunctive deps, we include all choices that are fully present
230 in |installed_db|. If none is present, we choose an arbitrary one that is
231 available.
232
233 Args:
234 dep_str: A raw dependency string.
235 installed_db: A database of installed packages.
236 avail_db: A database of packages available for installation.
237
238 Returns:
239 A list of pairs (CP, slot).
240
241 Raises:
242 ValueError: the dependencies string is malformed.
243 """
244 def ProcessSubDeps(dep_exp, disjunct):
245 """Parses and processes a dependency (sub)expression."""
246 deps = set()
247 default_deps = set()
248 sub_disjunct = False
249 for dep_sub_exp in dep_exp:
250 sub_deps = set()
251
252 if isinstance(dep_sub_exp, (list, tuple)):
253 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
254 sub_disjunct = False
255 elif sub_disjunct:
256 raise ValueError('Malformed disjunctive operation in deps')
257 elif dep_sub_exp == '||':
258 sub_disjunct = True
259 elif dep_sub_exp.endswith('?'):
260 raise ValueError('Dependencies contain a conditional')
261 else:
262 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
263 if cp:
264 sub_deps = set([(cp, slot)])
265 elif disjunct:
266 raise ValueError('Atom in disjunct ignored')
267
268 # Handle sub-deps of a disjunctive expression.
269 if disjunct:
270 # Make the first available choice the default, for use in case that
271 # no option is installed.
272 if (not default_deps and avail_db is not None and
273 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
274 default_deps = sub_deps
275
276 # If not all sub-deps are installed, then don't consider them.
277 if not all([cls._InDB(cp, slot, installed_db)
278 for cp, slot in sub_deps]):
279 sub_deps = set()
280
281 deps.update(sub_deps)
282
283 return deps or default_deps
284
285 try:
286 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
287 except portage.exception.InvalidDependString as e:
288 raise ValueError('Invalid dep string: %s' % e)
289 except ValueError as e:
290 raise ValueError('%s: %s' % (e, dep_str))
291
292 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
293 installed_db=None):
294 """Returns a database of packages given a list of CPV info.
295
296 Args:
297 cpv_info: A list of tuples containing package CPV and attributes.
298 process_rdeps: Whether to populate forward dependencies.
299 process_rev_rdeps: Whether to populate reverse dependencies.
300 installed_db: A database of installed packages for filtering disjunctive
301 choices against; if None, using own built database.
302
303 Returns:
304 A map from CP values to another dictionary that maps slots to package
305 attribute tuples. Tuples contain a CPV value (string), build time
306 (string), runtime dependencies (set), and reverse dependencies (set,
307 empty if not populated).
308
309 Raises:
310 ValueError: If more than one CPV occupies a single slot.
311 """
312 db = {}
313 logging.debug('Populating package DB...')
314 for cpv, slot, rdeps_raw, build_time in cpv_info:
315 cp = self._GetCP(cpv)
316 cp_slots = db.setdefault(cp, dict())
317 if slot in cp_slots:
318 raise ValueError('More than one package found for %s' %
319 self._AtomStr(cp, slot))
320 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
321 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
322 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
323
324 avail_db = db
325 if installed_db is None:
326 installed_db = db
327 avail_db = None
328
329 # Add approximate forward dependencies.
330 if process_rdeps:
331 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400332 for cp, cp_slots in db.items():
333 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700334 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
335 installed_db, avail_db))
336 logging.debug(' %s (%s) processed rdeps: %s',
337 self._AtomStr(cp, slot), pkg_info.cpv,
338 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
339 for rdep_cp, rdep_slot in pkg_info.rdeps]))
340
341 # Add approximate reverse dependencies (optional).
342 if process_rev_rdeps:
343 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400344 for cp, cp_slots in db.items():
345 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700346 for rdep_cp, rdep_slot in pkg_info.rdeps:
347 to_slots = db.get(rdep_cp)
348 if not to_slots:
349 continue
350
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400351 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700352 if rdep_slot and to_slot != rdep_slot:
353 continue
354 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
355 self._AtomStr(cp, slot), pkg_info.cpv,
356 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
357 to_pkg_info.rev_rdeps.add((cp, slot))
358
359 return db
360
361 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
362 """Initializes a dictionary of packages installed on |device|."""
363 get_vartree_script = self._GetVartreeSnippet(root)
364 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400365 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700366 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700367 except cros_build_lib.RunCommandError as e:
368 logging.error('Cannot get target vartree:\n%s', e.result.error)
369 raise
370
371 try:
372 self.target_db = self._BuildDB(json.loads(result.output),
373 process_rdeps, process_rev_rdeps)
374 except ValueError as e:
375 raise self.VartreeError(str(e))
376
377 def _InitBinpkgDB(self, process_rdeps):
378 """Initializes a dictionary of binary packages for updating the target."""
379 # Get build root trees; portage indexes require a trailing '/'.
380 build_root = os.path.join(self.sysroot, '')
381 trees = portage.create_trees(target_root=build_root, config_root=build_root)
382 bintree = trees[build_root]['bintree']
383 binpkgs_info = []
384 for cpv in bintree.dbapi.cpv_all():
385 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
386 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
387 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
388
389 try:
390 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
391 installed_db=self.target_db)
392 except ValueError as e:
393 raise self.BintreeError(str(e))
394
395 def _InitDepQueue(self):
396 """Initializes the dependency work queue."""
397 self.queue = set()
398 self.seen = {}
399 self.listed = set()
400
401 def _EnqDep(self, dep, listed, optional):
402 """Enqueues a dependency if not seen before or if turned non-optional."""
403 if dep in self.seen and (optional or not self.seen[dep]):
404 return False
405
406 self.queue.add(dep)
407 self.seen[dep] = optional
408 if listed:
409 self.listed.add(dep)
410 return True
411
412 def _DeqDep(self):
413 """Dequeues and returns a dependency, its listed and optional flags.
414
415 This returns listed packages first, if any are present, to ensure that we
416 correctly mark them as such when they are first being processed.
417 """
418 if self.listed:
419 dep = self.listed.pop()
420 self.queue.remove(dep)
421 listed = True
422 else:
423 dep = self.queue.pop()
424 listed = False
425
426 return dep, listed, self.seen[dep]
427
428 def _FindPackageMatches(self, cpv_pattern):
429 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
430
431 This is breaking |cpv_pattern| into its C, P and V components, each of
432 which may or may not be present or contain wildcards. It then scans the
433 binpkgs database to find all atoms that match these components, returning a
434 list of CP and slot qualifier. When the pattern does not specify a version,
435 or when a CP has only one slot in the binpkgs database, we omit the slot
436 qualifier in the result.
437
438 Args:
439 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
440
441 Returns:
442 A list of (CPV, slot) pairs of packages in the binpkgs database that
443 match the pattern.
444 """
445 attrs = portage_util.SplitCPV(cpv_pattern, strict=False)
446 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
447 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400448 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700449 if not fnmatch.fnmatchcase(cp, cp_pattern):
450 continue
451
452 # If no version attribute was given or there's only one slot, omit the
453 # slot qualifier.
454 if not attrs.version or len(cp_slots) == 1:
455 matches.append((cp, None))
456 else:
457 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400458 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700459 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
460 matches.append((cp, slot))
461
462 return matches
463
464 def _FindPackage(self, pkg):
465 """Returns the (CP, slot) pair for a package matching |pkg|.
466
467 Args:
468 pkg: Path to a binary package or a (partial) package CPV specifier.
469
470 Returns:
471 A (CP, slot) pair for the given package; slot may be None (unspecified).
472
473 Raises:
474 ValueError: if |pkg| is not a binpkg file nor does it match something
475 that's in the bintree.
476 """
477 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
478 package = os.path.basename(os.path.splitext(pkg)[0])
479 category = os.path.basename(os.path.dirname(pkg))
480 return self._GetCP(os.path.join(category, package)), None
481
482 matches = self._FindPackageMatches(pkg)
483 if not matches:
484 raise ValueError('No package found for %s' % pkg)
485
486 idx = 0
487 if len(matches) > 1:
488 # Ask user to pick among multiple matches.
489 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
490 ['%s:%s' % (cp, slot) if slot else cp
491 for cp, slot in matches])
492
493 return matches[idx]
494
495 def _NeedsInstall(self, cpv, slot, build_time, optional):
496 """Returns whether a package needs to be installed on the target.
497
498 Args:
499 cpv: Fully qualified CPV (string) of the package.
500 slot: Slot identifier (string).
501 build_time: The BUILT_TIME value (string) of the binpkg.
502 optional: Whether package is optional on the target.
503
504 Returns:
505 A tuple (install, update) indicating whether to |install| the package and
506 whether it is an |update| to an existing package.
507
508 Raises:
509 ValueError: if slot is not provided.
510 """
511 # If not checking installed packages, always install.
512 if not self.target_db:
513 return True, False
514
515 cp = self._GetCP(cpv)
516 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
517 if target_pkg_info is not None:
518 if cpv != target_pkg_info.cpv:
519 attrs = portage_util.SplitCPV(cpv)
520 target_attrs = portage_util.SplitCPV(target_pkg_info.cpv)
521 logging.debug('Updating %s: version (%s) different on target (%s)',
522 cp, attrs.version, target_attrs.version)
523 return True, True
524
525 if build_time != target_pkg_info.build_time:
526 logging.debug('Updating %s: build time (%s) different on target (%s)',
527 cpv, build_time, target_pkg_info.build_time)
528 return True, True
529
530 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
531 cp, target_pkg_info.cpv, target_pkg_info.build_time)
532 return False, False
533
534 if optional:
535 logging.debug('Not installing %s: missing on target but optional', cp)
536 return False, False
537
538 logging.debug('Installing %s: missing on target and non-optional (%s)',
539 cp, cpv)
540 return True, False
541
542 def _ProcessDeps(self, deps, reverse):
543 """Enqueues dependencies for processing.
544
545 Args:
546 deps: List of dependencies to enqueue.
547 reverse: Whether these are reverse dependencies.
548 """
549 if not deps:
550 return
551
552 logging.debug('Processing %d %s dep(s)...', len(deps),
553 'reverse' if reverse else 'forward')
554 num_already_seen = 0
555 for dep in deps:
556 if self._EnqDep(dep, False, reverse):
557 logging.debug(' Queued dep %s', dep)
558 else:
559 num_already_seen += 1
560
561 if num_already_seen:
562 logging.debug('%d dep(s) already seen', num_already_seen)
563
564 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
565 """Returns a dictionary of packages that need to be installed on the target.
566
567 Args:
568 process_rdeps: Whether to trace forward dependencies.
569 process_rev_rdeps: Whether to trace backward dependencies as well.
570
571 Returns:
572 A dictionary mapping CP values (string) to tuples containing a CPV
573 (string), a slot (string), a boolean indicating whether the package
574 was initially listed in the queue, and a boolean indicating whether this
575 is an update to an existing package.
576 """
577 installs = {}
578 while self.queue:
579 dep, listed, optional = self._DeqDep()
580 cp, required_slot = dep
581 if cp in installs:
582 logging.debug('Already updating %s', cp)
583 continue
584
585 cp_slots = self.binpkgs_db.get(cp, dict())
586 logging.debug('Checking packages matching %s%s%s...', cp,
587 ' (slot: %s)' % required_slot if required_slot else '',
588 ' (optional)' if optional else '')
589 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400590 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700591 if required_slot and slot != required_slot:
592 continue
593
594 num_processed += 1
595 logging.debug(' Checking %s...', pkg_info.cpv)
596
597 install, update = self._NeedsInstall(pkg_info.cpv, slot,
598 pkg_info.build_time, optional)
599 if not install:
600 continue
601
602 installs[cp] = (pkg_info.cpv, slot, listed, update)
603
604 # Add forward and backward runtime dependencies to queue.
605 if process_rdeps:
606 self._ProcessDeps(pkg_info.rdeps, False)
607 if process_rev_rdeps:
608 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
609 if target_pkg_info:
610 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
611
612 if num_processed == 0:
613 logging.warning('No qualified bintree package corresponding to %s', cp)
614
615 return installs
616
617 def _SortInstalls(self, installs):
618 """Returns a sorted list of packages to install.
619
620 Performs a topological sort based on dependencies found in the binary
621 package database.
622
623 Args:
624 installs: Dictionary of packages to install indexed by CP.
625
626 Returns:
627 A list of package CPVs (string).
628
629 Raises:
630 ValueError: If dependency graph contains a cycle.
631 """
632 not_visited = set(installs.keys())
633 curr_path = []
634 sorted_installs = []
635
636 def SortFrom(cp):
637 """Traverses dependencies recursively, emitting nodes in reverse order."""
638 cpv, slot, _, _ = installs[cp]
639 if cpv in curr_path:
640 raise ValueError('Dependencies contain a cycle: %s -> %s' %
641 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
642 curr_path.append(cpv)
643 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
644 if rdep_cp in not_visited:
645 not_visited.remove(rdep_cp)
646 SortFrom(rdep_cp)
647
648 sorted_installs.append(cpv)
649 curr_path.pop()
650
651 # So long as there's more packages, keep expanding dependency paths.
652 while not_visited:
653 SortFrom(not_visited.pop())
654
655 return sorted_installs
656
657 def _EnqListedPkg(self, pkg):
658 """Finds and enqueues a listed package."""
659 cp, slot = self._FindPackage(pkg)
660 if cp not in self.binpkgs_db:
661 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
662 self._EnqDep((cp, slot), True, False)
663
664 def _EnqInstalledPkgs(self):
665 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400666 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700667 target_cp_slots = self.target_db.get(cp)
668 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400669 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700670 if slot in target_cp_slots:
671 self._EnqDep((cp, slot), True, False)
672
673 def Run(self, device, root, listed_pkgs, update, process_rdeps,
674 process_rev_rdeps):
675 """Computes the list of packages that need to be installed on a target.
676
677 Args:
678 device: Target handler object.
679 root: Package installation root.
680 listed_pkgs: Package names/files listed by the user.
681 update: Whether to read the target's installed package database.
682 process_rdeps: Whether to trace forward dependencies.
683 process_rev_rdeps: Whether to trace backward dependencies as well.
684
685 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700686 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
687 list of package CPVs (string) to install on the target in an order that
688 satisfies their inter-dependencies, |listed| the subset that was
689 requested by the user, and |num_updates| the number of packages being
690 installed over preexisting versions. Note that installation order should
691 be reversed for removal, |install_attrs| is a dictionary mapping a package
692 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700693 """
694 if process_rev_rdeps and not process_rdeps:
695 raise ValueError('Must processing forward deps when processing rev deps')
696 if process_rdeps and not update:
697 raise ValueError('Must check installed packages when processing deps')
698
699 if update:
700 logging.info('Initializing target intalled packages database...')
701 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
702
703 logging.info('Initializing binary packages database...')
704 self._InitBinpkgDB(process_rdeps)
705
706 logging.info('Finding listed package(s)...')
707 self._InitDepQueue()
708 for pkg in listed_pkgs:
709 if pkg == '@installed':
710 if not update:
711 raise ValueError(
712 'Must check installed packages when updating all of them.')
713 self._EnqInstalledPkgs()
714 else:
715 self._EnqListedPkg(pkg)
716
717 logging.info('Computing set of packages to install...')
718 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
719
720 num_updates = 0
721 listed_installs = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400722 for cpv, _, listed, update in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700723 if listed:
724 listed_installs.append(cpv)
725 if update:
726 num_updates += 1
727
728 logging.info('Processed %d package(s), %d will be installed, %d are '
729 'updating existing packages',
730 len(self.seen), len(installs), num_updates)
731
732 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700733
734 install_attrs = {}
735 for pkg in sorted_installs:
736 pkg_path = os.path.join(root, portage.VDB_PATH, pkg)
737 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
738 install_attrs[pkg] = {}
739 if dlc_id and dlc_package:
740 install_attrs[pkg][_DLC_ID] = dlc_id
741
742 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700743
744
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700745def _Emerge(device, pkg_path, root, extra_args=None):
David Pursell9476bf42015-03-30 13:34:27 -0700746 """Copies |pkg| to |device| and emerges it.
747
748 Args:
749 device: A ChromiumOSDevice object.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700750 pkg_path: A path to a binary package.
David Pursell9476bf42015-03-30 13:34:27 -0700751 root: Package installation root path.
752 extra_args: Extra arguments to pass to emerge.
753
754 Raises:
755 DeployError: Unrecoverable error during emerge.
756 """
David Pursell9476bf42015-03-30 13:34:27 -0700757 pkgroot = os.path.join(device.work_dir, 'packages')
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700758 pkg_name = os.path.basename(pkg_path)
759 pkg_dirname = os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700760 pkg_dir = os.path.join(pkgroot, pkg_dirname)
Mike Frysinger15a4e012015-05-21 22:18:45 -0400761 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
762 # Clean out the dirs first if we had a previous emerge on the device so as to
763 # free up space for this emerge. The last emerge gets implicitly cleaned up
764 # when the device connection deletes its work_dir.
765 device.RunCommand(
766 ['rm', '-rf', pkg_dir, portage_tmpdir, '&&',
767 'mkdir', '-p', pkg_dir, portage_tmpdir], remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700768
Ralph Nathane01ccf12015-04-16 10:40:32 -0700769 # This message is read by BrilloDeployOperation.
770 logging.notice('Copying %s to device.', pkg_name)
Ilja H. Friedel0ab63e12017-03-28 13:29:48 -0700771 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700772
David Pursell9476bf42015-03-30 13:34:27 -0700773 logging.info('Use portage temp dir %s', portage_tmpdir)
774
Ralph Nathane01ccf12015-04-16 10:40:32 -0700775 # This message is read by BrilloDeployOperation.
776 logging.notice('Installing %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700777 pkg_path = os.path.join(pkg_dir, pkg_name)
778
779 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
780 # chromeos-base packages will be skipped due to the configuration
781 # in /etc/protage/make.profile/package.provided. However, there is
782 # a known bug that /usr/local/etc/portage is not setup properly
783 # (crbug.com/312041). This does not affect `cros deploy` because
784 # we do not use the preset PKGDIR.
785 extra_env = {
786 'FEATURES': '-sandbox',
787 'PKGDIR': pkgroot,
788 'PORTAGE_CONFIGROOT': '/usr/local',
789 'PORTAGE_TMPDIR': portage_tmpdir,
790 'PORTDIR': device.work_dir,
791 'CONFIG_PROTECT': '-*',
792 }
793 cmd = ['emerge', '--usepkg', pkg_path, '--root=%s' % root]
794 if extra_args:
795 cmd.append(extra_args)
796
797 try:
Greg Kerrb96c02c2019-02-08 14:32:41 -0800798 result = device.RunCommand(cmd, extra_env=extra_env, remote_sudo=True,
799 capture_output=True, debug_level=logging.INFO)
800
801 pattern = ('A requested package will not be merged because '
802 'it is listed in package.provided')
803 output = result.error.replace('\n', ' ').replace('\r', '')
804 if pattern in output:
805 error = ('Package failed to emerge: %s\n'
806 'Remove %s from /etc/portage/make.profile/'
807 'package.provided/chromeos-base.packages\n'
808 '(also see crbug.com/920140 for more context)\n'
809 % (pattern, pkg_name))
810 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700811 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700812 logging.error('Failed to emerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700813 raise
814 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700815 logging.notice('%s has been installed.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700816
817
Qijiang Fan8a945032019-04-25 20:53:29 +0900818def _HasSELinux(device):
819 """Check whether the device has SELinux-enabled.
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900820
821 Args:
822 device: A ChromiumOSDevice object.
823 """
824 try:
Qijiang Fan8a945032019-04-25 20:53:29 +0900825 device.CatFile('/sys/fs/selinux/enforce', max_size=None)
826 return True
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900827 except remote_access.CatFileError:
Qijiang Fan8a945032019-04-25 20:53:29 +0900828 return False
829
830
831def _IsSELinuxEnforced(device):
832 """Check whether the device has SELinux-enforced.
833
834 Args:
835 device: A ChromiumOSDevice object
836 """
837 return device.CatFile('/sys/fs/selinux/enforce', max_size=None).strip() == '1'
838
839
Qijiang Fand5958192019-07-26 12:32:36 +0900840def _RestoreSELinuxContext(device, pkgpath, root):
Qijiang Fan8a945032019-04-25 20:53:29 +0900841 """Restore SELinux context for files in a given pacakge.
842
843 This reads the tarball from pkgpath, and calls restorecon on device to
844 restore SELinux context for files listed in the tarball, assuming those files
845 are installed to /
846
847 Args:
848 device: a ChromiumOSDevice object
849 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900850 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900851 """
852 enforced = _IsSELinuxEnforced(device)
853 if enforced:
854 device.RunCommand(['setenforce', '0'])
855 pkgroot = os.path.join(device.work_dir, 'packages')
856 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
857 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
858 # Testing shows restorecon splits on newlines instead of spaces.
859 device.RunCommand(
Qijiang Fand5958192019-07-26 12:32:36 +0900860 ['cd', root, '&&',
861 'tar', 'tf', pkgpath_device, '|',
862 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900863 remote_sudo=True)
864 if enforced:
865 device.RunCommand(['setenforce', '1'])
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900866
867
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700868def _GetPackagesByCPV(cpvs, strip, sysroot):
869 """Returns paths to binary packages corresponding to |cpvs|.
870
871 Args:
872 cpvs: List of CPV components given by portage_util.SplitCPV().
873 strip: True to run strip_package.
874 sysroot: Sysroot path.
875
876 Returns:
877 List of paths corresponding to |cpvs|.
878
879 Raises:
880 DeployError: If a package is missing.
881 """
882 packages_dir = None
883 if strip:
884 try:
885 cros_build_lib.RunCommand(
886 ['strip_package', '--sysroot', sysroot] +
Alex Klein7078e252018-10-02 10:21:04 -0600887 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700888 packages_dir = _STRIPPED_PACKAGES_DIR
889 except cros_build_lib.RunCommandError:
890 logging.error('Cannot strip packages %s',
891 ' '.join([str(cpv) for cpv in cpvs]))
892 raise
893
894 paths = []
895 for cpv in cpvs:
896 path = portage_util.GetBinaryPackagePath(
897 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
898 packages_dir=packages_dir)
899 if not path:
900 raise DeployError('Missing package %s.' % cpv)
901 paths.append(path)
902
903 return paths
904
905
906def _GetPackagesPaths(pkgs, strip, sysroot):
907 """Returns paths to binary |pkgs|.
908
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700909 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700910 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700911 strip: Whether or not to run strip_package for CPV packages.
912 sysroot: The sysroot path.
913
914 Returns:
915 List of paths corresponding to |pkgs|.
916 """
Ned Nguyend0db4072019-02-22 14:19:21 -0700917 cpvs = [portage_util.SplitCPV(p) for p in pkgs]
918 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700919
920
David Pursell9476bf42015-03-30 13:34:27 -0700921def _Unmerge(device, pkg, root):
922 """Unmerges |pkg| on |device|.
923
924 Args:
925 device: A RemoteDevice object.
926 pkg: A package name.
927 root: Package installation root path.
928 """
Ralph Nathane01ccf12015-04-16 10:40:32 -0700929 pkg_name = os.path.basename(pkg)
930 # This message is read by BrilloDeployOperation.
931 logging.notice('Unmerging %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700932 cmd = ['qmerge', '--yes']
933 # Check if qmerge is available on the device. If not, use emerge.
934 if device.RunCommand(
935 ['qmerge', '--version'], error_code_ok=True).returncode != 0:
936 cmd = ['emerge']
937
938 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
939 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700940 # Always showing the emerge output for clarity.
David Pursell9476bf42015-03-30 13:34:27 -0700941 device.RunCommand(cmd, capture_output=False, remote_sudo=True,
942 debug_level=logging.INFO)
943 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700944 logging.error('Failed to unmerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700945 raise
946 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700947 logging.notice('%s has been uninstalled.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700948
949
950def _ConfirmDeploy(num_updates):
951 """Returns whether we can continue deployment."""
952 if num_updates > _MAX_UPDATES_NUM:
953 logging.warning(_MAX_UPDATES_WARNING)
954 return cros_build_lib.BooleanPrompt(default=False)
955
956 return True
957
958
Ralph Nathane01ccf12015-04-16 10:40:32 -0700959def _EmergePackages(pkgs, device, strip, sysroot, root, emerge_args):
960 """Call _Emerge for each packge in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700961 dlc_deployed = False
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700962 for pkg_path in _GetPackagesPaths(pkgs, strip, sysroot):
963 _Emerge(device, pkg_path, root, extra_args=emerge_args)
Qijiang Fan8a945032019-04-25 20:53:29 +0900964 if _HasSELinux(device):
Qijiang Fand5958192019-07-26 12:32:36 +0900965 _RestoreSELinuxContext(device, pkg_path, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700966 if _DeployDLCImage(device, pkg_path):
967 dlc_deployed = True
968
969 # Restart dlcservice so it picks up the newly installed DLC modules (in case
970 # we installed new DLC images).
971 if dlc_deployed:
972 device.RunCommand(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -0700973
974
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700975def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -0700976 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700977 dlc_uninstalled = False
Ralph Nathane01ccf12015-04-16 10:40:32 -0700978 for pkg in pkgs:
979 _Unmerge(device, pkg, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700980 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
981 dlc_uninstalled = True
982
983 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
984 # uninstalled DLC images).
985 if dlc_uninstalled:
986 device.RunCommand(['restart', 'dlcservice'])
987
988
989def _UninstallDLCImage(device, pkg_attrs):
990 """Uninstall a DLC image."""
991 if _DLC_ID in pkg_attrs:
992 dlc_id = pkg_attrs[_DLC_ID]
993 logging.notice('Uninstalling DLC image for %s', dlc_id)
994
995 device.RunCommand(['sudo', '-u', 'chronos', 'dlcservice_util',
996 '--uninstall', '--dlc_ids=%s' % dlc_id])
997 return True
998 else:
999 logging.debug('DLC_ID not found in package')
1000 return False
1001
1002
1003def _DeployDLCImage(device, pkg_path):
1004 """Deploy (install and mount) a DLC image."""
1005 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
1006 if dlc_id and dlc_package:
1007 logging.notice('Deploy a DLC image for %s', dlc_id)
1008
1009 dlc_path_src = os.path.join('/build/rootfs/dlc', dlc_id, dlc_package,
1010 'dlc.img')
1011 dlc_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1012 dlc_path_a = os.path.join(dlc_path, 'dlc_a')
1013 dlc_path_b = os.path.join(dlc_path, 'dlc_b')
1014 # Create folders for DLC images.
1015 device.RunCommand(['mkdir', '-p', dlc_path_a, dlc_path_b])
1016 # Copy images to the destination folders.
1017 device.RunCommand(['cp', dlc_path_src,
1018 os.path.join(dlc_path_a, 'dlc.img')])
1019 device.RunCommand(['cp', dlc_path_src,
1020 os.path.join(dlc_path_b, 'dlc.img')])
1021
1022 # Set the proper perms and ownership so dlcservice can access the image.
1023 device.RunCommand(['chmod', '-R', '0755', _DLC_INSTALL_ROOT])
1024 device.RunCommand(['chown', '-R', 'dlcservice:dlcservice',
1025 _DLC_INSTALL_ROOT])
1026 return True
1027 else:
1028 logging.debug('DLC_ID not found in package')
1029 return False
1030
1031
1032def _GetDLCInfo(device, pkg_path, from_dut):
1033 """Returns information of a DLC given its package path.
1034
1035 Args:
1036 device: commandline.Device object; None to use the default device.
1037 pkg_path: path to the package.
1038 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1039 info from host.
1040
1041 Returns:
1042 A tuple (dlc_id, dlc_package).
1043 """
1044 environment_content = ''
1045 if from_dut:
1046 # On DUT, |pkg_path| is the directory which contains environment file.
1047 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1048 result = device.RunCommand(['test', '-f', environment_path],
1049 error_code_ok=True)
1050 if result.returncode == 1:
1051 # The package is not installed on DUT yet. Skip extracting info.
1052 return None, None
1053 result = device.RunCommand(['bzip2', '-d', '-c', environment_path])
1054 environment_content = result.output
1055 else:
1056 # On host, pkg_path is tbz2 file which contains environment file.
1057 # Extract the metadata of the package file.
1058 data = portage.xpak.tbz2(pkg_path).get_data()
1059 # Extract the environment metadata.
1060 environment_content = bz2.decompress(data[_ENVIRONMENT_FILENAME])
1061
1062 with tempfile.NamedTemporaryFile() as f:
1063 # Dumps content into a file so we can use osutils.SourceEnvironment.
1064 path = os.path.realpath(f.name)
1065 osutils.WriteFile(path, environment_content)
1066 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE))
1067 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001068
1069
Gilad Arnolda0a98062015-07-07 08:34:27 -07001070def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1071 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1072 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1073 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001074 """Deploys packages to a device.
1075
1076 Args:
David Pursell2e773382015-04-03 14:30:47 -07001077 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001078 packages: List of packages (strings) to deploy to device.
1079 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001080 emerge: True to emerge package, False to unmerge.
1081 update: Check installed version on device.
1082 deep: Install dependencies also. Implies |update|.
1083 deep_rev: Install reverse dependencies. Implies |deep|.
1084 clean_binpkg: Clean outdated binary packages.
1085 root: Package installation root path.
1086 strip: Run strip_package to filter out preset paths in the package.
1087 emerge_args: Extra arguments to pass to emerge.
1088 ssh_private_key: Path to an SSH private key file; None to use test keys.
1089 ping: True to ping the device before trying to connect.
1090 force: Ignore sanity checks and prompts.
1091 dry_run: Print deployment plan but do not deploy anything.
1092
1093 Raises:
1094 ValueError: Invalid parameter or parameter combination.
1095 DeployError: Unrecoverable failure during deploy.
1096 """
1097 if deep_rev:
1098 deep = True
1099 if deep:
1100 update = True
1101
Gilad Arnolda0a98062015-07-07 08:34:27 -07001102 if not packages:
1103 raise DeployError('No packages provided, nothing to deploy.')
1104
David Pursell9476bf42015-03-30 13:34:27 -07001105 if update and not emerge:
1106 raise ValueError('Cannot update and unmerge.')
1107
David Pursell2e773382015-04-03 14:30:47 -07001108 if device:
1109 hostname, username, port = device.hostname, device.username, device.port
1110 else:
1111 hostname, username, port = None, None, None
1112
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001113 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001114 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001115 try:
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001116 with remote_access.ChromiumOSDeviceHandler(
1117 hostname, port=port, username=username, private_key=ssh_private_key,
1118 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001119 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001120
Gilad Arnolda0a98062015-07-07 08:34:27 -07001121 board = cros_build_lib.GetBoard(device_board=device.board,
1122 override_board=board)
1123 if not force and board != device.board:
1124 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001125 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001126
Gilad Arnolda0a98062015-07-07 08:34:27 -07001127 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -07001128
1129 if clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001130 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001131 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001132
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001133 # Remount rootfs as writable if necessary.
1134 if not device.MountRootfsReadWrite():
1135 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001136
1137 # Obtain list of packages to upgrade/remove.
1138 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001139 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001140 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001141 if emerge:
1142 action_str = 'emerge'
1143 else:
1144 pkgs.reverse()
1145 action_str = 'unmerge'
1146
1147 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001148 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001149 return
1150
Ralph Nathane01ccf12015-04-16 10:40:32 -07001151 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001152 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001153 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001154
1155 if dry_run or not _ConfirmDeploy(num_updates):
1156 return
1157
Ralph Nathane01ccf12015-04-16 10:40:32 -07001158 # Select function (emerge or unmerge) and bind args.
1159 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001160 func = functools.partial(_EmergePackages, pkgs, device, strip,
Ralph Nathane01ccf12015-04-16 10:40:32 -07001161 sysroot, root, emerge_args)
1162 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001163 func = functools.partial(_UnmergePackages, pkgs, device, root,
1164 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001165
1166 # Call the function with the progress bar or with normal output.
1167 if command.UseProgressBar():
1168 op = BrilloDeployOperation(len(pkgs), emerge)
1169 op.Run(func, log_level=logging.DEBUG)
1170 else:
1171 func()
David Pursell9476bf42015-03-30 13:34:27 -07001172
Qijiang Fan8a945032019-04-25 20:53:29 +09001173 if _HasSELinux(device):
1174 if sum(x.count('selinux-policy') for x in pkgs):
1175 logging.warning(
1176 'Deploying SELinux policy will not take effect until reboot. '
1177 'SELinux policy is loaded by init. Also, security contexts '
1178 '(labels) in files will require manual relabeling by the user '
1179 'if your policy modifies the file contexts.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001180
David Pursell9476bf42015-03-30 13:34:27 -07001181 logging.warning('Please restart any updated services on the device, '
1182 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001183 except Exception:
1184 if lsb_release:
1185 lsb_entries = sorted(lsb_release.items())
1186 logging.info('Following are the LSB version details of the device:\n%s',
1187 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1188 raise