blob: 3fa5f1dd9909b83fb71d3291cd82bcc2bb7e60a4 [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
Mike Frysinger93e8ffa2019-07-03 20:24:18 -04008from __future__ import division
David Pursell9476bf42015-03-30 13:34:27 -07009from __future__ import print_function
10
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070011import bz2
David Pursell9476bf42015-03-30 13:34:27 -070012import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070013import functools
David Pursell9476bf42015-03-30 13:34:27 -070014import json
15import os
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070016import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070017
Ralph Nathane01ccf12015-04-16 10:40:32 -070018from chromite.cli import command
David Pursell9476bf42015-03-30 13:34:27 -070019from chromite.lib import cros_build_lib
20from chromite.lib import cros_logging as logging
Ralph Nathane01ccf12015-04-16 10:40:32 -070021from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070022from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070023from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070024from chromite.lib import remote_access
25try:
26 import portage
27except ImportError:
28 if cros_build_lib.IsInsideChroot():
29 raise
30
31
32_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
33# This is defined in src/platform/dev/builder.py
34_STRIPPED_PACKAGES_DIR = 'stripped-packages'
35
36_MAX_UPDATES_NUM = 10
37_MAX_UPDATES_WARNING = (
38 'You are about to update a large number of installed packages, which '
39 'might take a long time, fail midway, or leave the target in an '
40 'inconsistent state. It is highly recommended that you flash a new image '
41 'instead.')
42
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070043_DLC_ID = 'DLC_ID'
44_DLC_PACKAGE = 'DLC_PACKAGE'
45_ENVIRONMENT_FILENAME = 'environment.bz2'
46_DLC_INSTALL_ROOT = '/var/cache/dlc'
47
David Pursell9476bf42015-03-30 13:34:27 -070048
49class DeployError(Exception):
50 """Thrown when an unrecoverable error is encountered during deploy."""
51
52
Ralph Nathane01ccf12015-04-16 10:40:32 -070053class BrilloDeployOperation(operation.ProgressBarOperation):
54 """ProgressBarOperation specific for brillo deploy."""
Ralph Nathan90475a12015-05-20 13:19:01 -070055 MERGE_EVENTS = ['NOTICE: Copying', 'NOTICE: Installing',
Achuith Bhandarkar0487c312019-04-22 12:19:25 -070056 'emerge --usepkg', 'has been installed.']
Ralph Nathan90475a12015-05-20 13:19:01 -070057 UNMERGE_EVENTS = ['NOTICE: Unmerging', 'has been uninstalled.']
Ralph Nathane01ccf12015-04-16 10:40:32 -070058
59 def __init__(self, pkg_count, emerge):
60 """Construct BrilloDeployOperation object.
61
62 Args:
63 pkg_count: number of packages being built.
64 emerge: True if emerge, False is unmerge.
65 """
66 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070067 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070068 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070069 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070070 self._events = self.UNMERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070071 self._total = pkg_count * len(self._events)
72 self._completed = 0
73
Ralph Nathandc14ed92015-04-22 11:17:40 -070074 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070075 """Parse the output of brillo deploy to update a progress bar."""
76 stdout = self._stdout.read()
77 stderr = self._stderr.read()
78 output = stdout + stderr
79 for event in self._events:
80 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040081 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -070082
83
David Pursell9476bf42015-03-30 13:34:27 -070084class _InstallPackageScanner(object):
85 """Finds packages that need to be installed on a target device.
86
87 Scans the sysroot bintree, beginning with a user-provided list of packages,
88 to find all packages that need to be installed. If so instructed,
89 transitively scans forward (mandatory) and backward (optional) dependencies
90 as well. A package will be installed if missing on the target (mandatory
91 packages only), or it will be updated if its sysroot version and build time
92 are different from the target. Common usage:
93
94 pkg_scanner = _InstallPackageScanner(sysroot)
95 pkgs = pkg_scanner.Run(...)
96 """
97
98 class VartreeError(Exception):
99 """An error in the processing of the installed packages tree."""
100
101 class BintreeError(Exception):
102 """An error in the processing of the source binpkgs tree."""
103
104 class PkgInfo(object):
105 """A record containing package information."""
106
107 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
108
109 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
110 self.cpv = cpv
111 self.build_time = build_time
112 self.rdeps_raw = rdeps_raw
113 self.rdeps = set() if rdeps is None else rdeps
114 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
115
116 # Python snippet for dumping vartree info on the target. Instantiate using
117 # _GetVartreeSnippet().
118 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700119import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700120import os
121import portage
122
123# Normalize the path to match what portage will index.
124target_root = os.path.normpath('%(root)s')
125if not target_root.endswith('/'):
126 target_root += '/'
127trees = portage.create_trees(target_root=target_root, config_root='/')
128vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700129pkg_info = []
130for cpv in vartree.dbapi.cpv_all():
131 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
132 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
133 pkg_info.append((cpv, slot, rdep_raw, build_time))
134
135print(json.dumps(pkg_info))
136"""
137
138 def __init__(self, sysroot):
139 self.sysroot = sysroot
140 # Members containing the sysroot (binpkg) and target (installed) package DB.
141 self.target_db = None
142 self.binpkgs_db = None
143 # Members for managing the dependency resolution work queue.
144 self.queue = None
145 self.seen = None
146 self.listed = None
147
148 @staticmethod
149 def _GetCP(cpv):
150 """Returns the CP value for a given CPV string."""
151 attrs = portage_util.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600152 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700153 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600154 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700155
156 @staticmethod
157 def _InDB(cp, slot, db):
158 """Returns whether CP and slot are found in a database (if provided)."""
159 cp_slots = db.get(cp) if db else None
160 return cp_slots is not None and (not slot or slot in cp_slots)
161
162 @staticmethod
163 def _AtomStr(cp, slot):
164 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
165 return '%s:%s' % (cp, slot) if slot else cp
166
167 @classmethod
168 def _GetVartreeSnippet(cls, root='/'):
169 """Returns a code snippet for dumping the vartree on the target.
170
171 Args:
172 root: The installation root.
173
174 Returns:
175 The said code snippet (string) with parameters filled in.
176 """
177 return cls._GET_VARTREE % {'root': root}
178
179 @classmethod
180 def _StripDepAtom(cls, dep_atom, installed_db=None):
181 """Strips a dependency atom and returns a (CP, slot) pair."""
182 # TODO(garnold) This is a gross simplification of ebuild dependency
183 # semantics, stripping and ignoring various qualifiers (versions, slots,
184 # USE flag, negation) and will likely need to be fixed. chromium:447366.
185
186 # Ignore unversioned blockers, leaving them for the user to resolve.
187 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
188 return None, None
189
190 cp = dep_atom
191 slot = None
192 require_installed = False
193
194 # Versioned blockers should be updated, but only if already installed.
195 # These are often used for forcing cascaded updates of multiple packages,
196 # so we're treating them as ordinary constraints with hopes that it'll lead
197 # to the desired result.
198 if cp.startswith('!'):
199 cp = cp.lstrip('!')
200 require_installed = True
201
202 # Remove USE flags.
203 if '[' in cp:
204 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
205
206 # Separate the slot qualifier and strip off subslots.
207 if ':' in cp:
208 cp, slot = cp.split(':')
209 for delim in ('/', '='):
210 slot = slot.split(delim, 1)[0]
211
212 # Strip version wildcards (right), comparators (left).
213 cp = cp.rstrip('*')
214 cp = cp.lstrip('<=>~')
215
216 # Turn into CP form.
217 cp = cls._GetCP(cp)
218
219 if require_installed and not cls._InDB(cp, None, installed_db):
220 return None, None
221
222 return cp, slot
223
224 @classmethod
225 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
226 """Resolves and returns a list of dependencies from a dependency string.
227
228 This parses a dependency string and returns a list of package names and
229 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
230 resolving disjunctive deps, we include all choices that are fully present
231 in |installed_db|. If none is present, we choose an arbitrary one that is
232 available.
233
234 Args:
235 dep_str: A raw dependency string.
236 installed_db: A database of installed packages.
237 avail_db: A database of packages available for installation.
238
239 Returns:
240 A list of pairs (CP, slot).
241
242 Raises:
243 ValueError: the dependencies string is malformed.
244 """
245 def ProcessSubDeps(dep_exp, disjunct):
246 """Parses and processes a dependency (sub)expression."""
247 deps = set()
248 default_deps = set()
249 sub_disjunct = False
250 for dep_sub_exp in dep_exp:
251 sub_deps = set()
252
253 if isinstance(dep_sub_exp, (list, tuple)):
254 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
255 sub_disjunct = False
256 elif sub_disjunct:
257 raise ValueError('Malformed disjunctive operation in deps')
258 elif dep_sub_exp == '||':
259 sub_disjunct = True
260 elif dep_sub_exp.endswith('?'):
261 raise ValueError('Dependencies contain a conditional')
262 else:
263 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
264 if cp:
265 sub_deps = set([(cp, slot)])
266 elif disjunct:
267 raise ValueError('Atom in disjunct ignored')
268
269 # Handle sub-deps of a disjunctive expression.
270 if disjunct:
271 # Make the first available choice the default, for use in case that
272 # no option is installed.
273 if (not default_deps and avail_db is not None and
274 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
275 default_deps = sub_deps
276
277 # If not all sub-deps are installed, then don't consider them.
278 if not all([cls._InDB(cp, slot, installed_db)
279 for cp, slot in sub_deps]):
280 sub_deps = set()
281
282 deps.update(sub_deps)
283
284 return deps or default_deps
285
286 try:
287 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
288 except portage.exception.InvalidDependString as e:
289 raise ValueError('Invalid dep string: %s' % e)
290 except ValueError as e:
291 raise ValueError('%s: %s' % (e, dep_str))
292
293 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
294 installed_db=None):
295 """Returns a database of packages given a list of CPV info.
296
297 Args:
298 cpv_info: A list of tuples containing package CPV and attributes.
299 process_rdeps: Whether to populate forward dependencies.
300 process_rev_rdeps: Whether to populate reverse dependencies.
301 installed_db: A database of installed packages for filtering disjunctive
302 choices against; if None, using own built database.
303
304 Returns:
305 A map from CP values to another dictionary that maps slots to package
306 attribute tuples. Tuples contain a CPV value (string), build time
307 (string), runtime dependencies (set), and reverse dependencies (set,
308 empty if not populated).
309
310 Raises:
311 ValueError: If more than one CPV occupies a single slot.
312 """
313 db = {}
314 logging.debug('Populating package DB...')
315 for cpv, slot, rdeps_raw, build_time in cpv_info:
316 cp = self._GetCP(cpv)
317 cp_slots = db.setdefault(cp, dict())
318 if slot in cp_slots:
319 raise ValueError('More than one package found for %s' %
320 self._AtomStr(cp, slot))
321 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
322 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
323 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
324
325 avail_db = db
326 if installed_db is None:
327 installed_db = db
328 avail_db = None
329
330 # Add approximate forward dependencies.
331 if process_rdeps:
332 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400333 for cp, cp_slots in db.items():
334 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700335 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
336 installed_db, avail_db))
337 logging.debug(' %s (%s) processed rdeps: %s',
338 self._AtomStr(cp, slot), pkg_info.cpv,
339 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
340 for rdep_cp, rdep_slot in pkg_info.rdeps]))
341
342 # Add approximate reverse dependencies (optional).
343 if process_rev_rdeps:
344 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400345 for cp, cp_slots in db.items():
346 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700347 for rdep_cp, rdep_slot in pkg_info.rdeps:
348 to_slots = db.get(rdep_cp)
349 if not to_slots:
350 continue
351
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400352 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700353 if rdep_slot and to_slot != rdep_slot:
354 continue
355 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
356 self._AtomStr(cp, slot), pkg_info.cpv,
357 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
358 to_pkg_info.rev_rdeps.add((cp, slot))
359
360 return db
361
362 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
363 """Initializes a dictionary of packages installed on |device|."""
364 get_vartree_script = self._GetVartreeSnippet(root)
365 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400366 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700367 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700368 except cros_build_lib.RunCommandError as e:
369 logging.error('Cannot get target vartree:\n%s', e.result.error)
370 raise
371
372 try:
373 self.target_db = self._BuildDB(json.loads(result.output),
374 process_rdeps, process_rev_rdeps)
375 except ValueError as e:
376 raise self.VartreeError(str(e))
377
378 def _InitBinpkgDB(self, process_rdeps):
379 """Initializes a dictionary of binary packages for updating the target."""
380 # Get build root trees; portage indexes require a trailing '/'.
381 build_root = os.path.join(self.sysroot, '')
382 trees = portage.create_trees(target_root=build_root, config_root=build_root)
383 bintree = trees[build_root]['bintree']
384 binpkgs_info = []
385 for cpv in bintree.dbapi.cpv_all():
386 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
387 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
388 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
389
390 try:
391 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
392 installed_db=self.target_db)
393 except ValueError as e:
394 raise self.BintreeError(str(e))
395
396 def _InitDepQueue(self):
397 """Initializes the dependency work queue."""
398 self.queue = set()
399 self.seen = {}
400 self.listed = set()
401
402 def _EnqDep(self, dep, listed, optional):
403 """Enqueues a dependency if not seen before or if turned non-optional."""
404 if dep in self.seen and (optional or not self.seen[dep]):
405 return False
406
407 self.queue.add(dep)
408 self.seen[dep] = optional
409 if listed:
410 self.listed.add(dep)
411 return True
412
413 def _DeqDep(self):
414 """Dequeues and returns a dependency, its listed and optional flags.
415
416 This returns listed packages first, if any are present, to ensure that we
417 correctly mark them as such when they are first being processed.
418 """
419 if self.listed:
420 dep = self.listed.pop()
421 self.queue.remove(dep)
422 listed = True
423 else:
424 dep = self.queue.pop()
425 listed = False
426
427 return dep, listed, self.seen[dep]
428
429 def _FindPackageMatches(self, cpv_pattern):
430 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
431
432 This is breaking |cpv_pattern| into its C, P and V components, each of
433 which may or may not be present or contain wildcards. It then scans the
434 binpkgs database to find all atoms that match these components, returning a
435 list of CP and slot qualifier. When the pattern does not specify a version,
436 or when a CP has only one slot in the binpkgs database, we omit the slot
437 qualifier in the result.
438
439 Args:
440 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
441
442 Returns:
443 A list of (CPV, slot) pairs of packages in the binpkgs database that
444 match the pattern.
445 """
446 attrs = portage_util.SplitCPV(cpv_pattern, strict=False)
447 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
448 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400449 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700450 if not fnmatch.fnmatchcase(cp, cp_pattern):
451 continue
452
453 # If no version attribute was given or there's only one slot, omit the
454 # slot qualifier.
455 if not attrs.version or len(cp_slots) == 1:
456 matches.append((cp, None))
457 else:
458 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400459 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700460 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
461 matches.append((cp, slot))
462
463 return matches
464
465 def _FindPackage(self, pkg):
466 """Returns the (CP, slot) pair for a package matching |pkg|.
467
468 Args:
469 pkg: Path to a binary package or a (partial) package CPV specifier.
470
471 Returns:
472 A (CP, slot) pair for the given package; slot may be None (unspecified).
473
474 Raises:
475 ValueError: if |pkg| is not a binpkg file nor does it match something
476 that's in the bintree.
477 """
478 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
479 package = os.path.basename(os.path.splitext(pkg)[0])
480 category = os.path.basename(os.path.dirname(pkg))
481 return self._GetCP(os.path.join(category, package)), None
482
483 matches = self._FindPackageMatches(pkg)
484 if not matches:
485 raise ValueError('No package found for %s' % pkg)
486
487 idx = 0
488 if len(matches) > 1:
489 # Ask user to pick among multiple matches.
490 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
491 ['%s:%s' % (cp, slot) if slot else cp
492 for cp, slot in matches])
493
494 return matches[idx]
495
496 def _NeedsInstall(self, cpv, slot, build_time, optional):
497 """Returns whether a package needs to be installed on the target.
498
499 Args:
500 cpv: Fully qualified CPV (string) of the package.
501 slot: Slot identifier (string).
502 build_time: The BUILT_TIME value (string) of the binpkg.
503 optional: Whether package is optional on the target.
504
505 Returns:
506 A tuple (install, update) indicating whether to |install| the package and
507 whether it is an |update| to an existing package.
508
509 Raises:
510 ValueError: if slot is not provided.
511 """
512 # If not checking installed packages, always install.
513 if not self.target_db:
514 return True, False
515
516 cp = self._GetCP(cpv)
517 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
518 if target_pkg_info is not None:
519 if cpv != target_pkg_info.cpv:
520 attrs = portage_util.SplitCPV(cpv)
521 target_attrs = portage_util.SplitCPV(target_pkg_info.cpv)
522 logging.debug('Updating %s: version (%s) different on target (%s)',
523 cp, attrs.version, target_attrs.version)
524 return True, True
525
526 if build_time != target_pkg_info.build_time:
527 logging.debug('Updating %s: build time (%s) different on target (%s)',
528 cpv, build_time, target_pkg_info.build_time)
529 return True, True
530
531 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
532 cp, target_pkg_info.cpv, target_pkg_info.build_time)
533 return False, False
534
535 if optional:
536 logging.debug('Not installing %s: missing on target but optional', cp)
537 return False, False
538
539 logging.debug('Installing %s: missing on target and non-optional (%s)',
540 cp, cpv)
541 return True, False
542
543 def _ProcessDeps(self, deps, reverse):
544 """Enqueues dependencies for processing.
545
546 Args:
547 deps: List of dependencies to enqueue.
548 reverse: Whether these are reverse dependencies.
549 """
550 if not deps:
551 return
552
553 logging.debug('Processing %d %s dep(s)...', len(deps),
554 'reverse' if reverse else 'forward')
555 num_already_seen = 0
556 for dep in deps:
557 if self._EnqDep(dep, False, reverse):
558 logging.debug(' Queued dep %s', dep)
559 else:
560 num_already_seen += 1
561
562 if num_already_seen:
563 logging.debug('%d dep(s) already seen', num_already_seen)
564
565 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
566 """Returns a dictionary of packages that need to be installed on the target.
567
568 Args:
569 process_rdeps: Whether to trace forward dependencies.
570 process_rev_rdeps: Whether to trace backward dependencies as well.
571
572 Returns:
573 A dictionary mapping CP values (string) to tuples containing a CPV
574 (string), a slot (string), a boolean indicating whether the package
575 was initially listed in the queue, and a boolean indicating whether this
576 is an update to an existing package.
577 """
578 installs = {}
579 while self.queue:
580 dep, listed, optional = self._DeqDep()
581 cp, required_slot = dep
582 if cp in installs:
583 logging.debug('Already updating %s', cp)
584 continue
585
586 cp_slots = self.binpkgs_db.get(cp, dict())
587 logging.debug('Checking packages matching %s%s%s...', cp,
588 ' (slot: %s)' % required_slot if required_slot else '',
589 ' (optional)' if optional else '')
590 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400591 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700592 if required_slot and slot != required_slot:
593 continue
594
595 num_processed += 1
596 logging.debug(' Checking %s...', pkg_info.cpv)
597
598 install, update = self._NeedsInstall(pkg_info.cpv, slot,
599 pkg_info.build_time, optional)
600 if not install:
601 continue
602
603 installs[cp] = (pkg_info.cpv, slot, listed, update)
604
605 # Add forward and backward runtime dependencies to queue.
606 if process_rdeps:
607 self._ProcessDeps(pkg_info.rdeps, False)
608 if process_rev_rdeps:
609 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
610 if target_pkg_info:
611 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
612
613 if num_processed == 0:
614 logging.warning('No qualified bintree package corresponding to %s', cp)
615
616 return installs
617
618 def _SortInstalls(self, installs):
619 """Returns a sorted list of packages to install.
620
621 Performs a topological sort based on dependencies found in the binary
622 package database.
623
624 Args:
625 installs: Dictionary of packages to install indexed by CP.
626
627 Returns:
628 A list of package CPVs (string).
629
630 Raises:
631 ValueError: If dependency graph contains a cycle.
632 """
633 not_visited = set(installs.keys())
634 curr_path = []
635 sorted_installs = []
636
637 def SortFrom(cp):
638 """Traverses dependencies recursively, emitting nodes in reverse order."""
639 cpv, slot, _, _ = installs[cp]
640 if cpv in curr_path:
641 raise ValueError('Dependencies contain a cycle: %s -> %s' %
642 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
643 curr_path.append(cpv)
644 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
645 if rdep_cp in not_visited:
646 not_visited.remove(rdep_cp)
647 SortFrom(rdep_cp)
648
649 sorted_installs.append(cpv)
650 curr_path.pop()
651
652 # So long as there's more packages, keep expanding dependency paths.
653 while not_visited:
654 SortFrom(not_visited.pop())
655
656 return sorted_installs
657
658 def _EnqListedPkg(self, pkg):
659 """Finds and enqueues a listed package."""
660 cp, slot = self._FindPackage(pkg)
661 if cp not in self.binpkgs_db:
662 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
663 self._EnqDep((cp, slot), True, False)
664
665 def _EnqInstalledPkgs(self):
666 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400667 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700668 target_cp_slots = self.target_db.get(cp)
669 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400670 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700671 if slot in target_cp_slots:
672 self._EnqDep((cp, slot), True, False)
673
674 def Run(self, device, root, listed_pkgs, update, process_rdeps,
675 process_rev_rdeps):
676 """Computes the list of packages that need to be installed on a target.
677
678 Args:
679 device: Target handler object.
680 root: Package installation root.
681 listed_pkgs: Package names/files listed by the user.
682 update: Whether to read the target's installed package database.
683 process_rdeps: Whether to trace forward dependencies.
684 process_rev_rdeps: Whether to trace backward dependencies as well.
685
686 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700687 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
688 list of package CPVs (string) to install on the target in an order that
689 satisfies their inter-dependencies, |listed| the subset that was
690 requested by the user, and |num_updates| the number of packages being
691 installed over preexisting versions. Note that installation order should
692 be reversed for removal, |install_attrs| is a dictionary mapping a package
693 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700694 """
695 if process_rev_rdeps and not process_rdeps:
696 raise ValueError('Must processing forward deps when processing rev deps')
697 if process_rdeps and not update:
698 raise ValueError('Must check installed packages when processing deps')
699
700 if update:
701 logging.info('Initializing target intalled packages database...')
702 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
703
704 logging.info('Initializing binary packages database...')
705 self._InitBinpkgDB(process_rdeps)
706
707 logging.info('Finding listed package(s)...')
708 self._InitDepQueue()
709 for pkg in listed_pkgs:
710 if pkg == '@installed':
711 if not update:
712 raise ValueError(
713 'Must check installed packages when updating all of them.')
714 self._EnqInstalledPkgs()
715 else:
716 self._EnqListedPkg(pkg)
717
718 logging.info('Computing set of packages to install...')
719 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
720
721 num_updates = 0
722 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400723 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700724 if listed:
725 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400726 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700727 num_updates += 1
728
729 logging.info('Processed %d package(s), %d will be installed, %d are '
730 'updating existing packages',
731 len(self.seen), len(installs), num_updates)
732
733 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700734
735 install_attrs = {}
736 for pkg in sorted_installs:
737 pkg_path = os.path.join(root, portage.VDB_PATH, pkg)
738 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
739 install_attrs[pkg] = {}
740 if dlc_id and dlc_package:
741 install_attrs[pkg][_DLC_ID] = dlc_id
742
743 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700744
745
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700746def _Emerge(device, pkg_path, root, extra_args=None):
David Pursell9476bf42015-03-30 13:34:27 -0700747 """Copies |pkg| to |device| and emerges it.
748
749 Args:
750 device: A ChromiumOSDevice object.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700751 pkg_path: A path to a binary package.
David Pursell9476bf42015-03-30 13:34:27 -0700752 root: Package installation root path.
753 extra_args: Extra arguments to pass to emerge.
754
755 Raises:
756 DeployError: Unrecoverable error during emerge.
757 """
David Pursell9476bf42015-03-30 13:34:27 -0700758 pkgroot = os.path.join(device.work_dir, 'packages')
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700759 pkg_name = os.path.basename(pkg_path)
760 pkg_dirname = os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700761 pkg_dir = os.path.join(pkgroot, pkg_dirname)
Mike Frysinger15a4e012015-05-21 22:18:45 -0400762 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
763 # Clean out the dirs first if we had a previous emerge on the device so as to
764 # free up space for this emerge. The last emerge gets implicitly cleaned up
765 # when the device connection deletes its work_dir.
766 device.RunCommand(
767 ['rm', '-rf', pkg_dir, portage_tmpdir, '&&',
768 'mkdir', '-p', pkg_dir, portage_tmpdir], remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700769
Ralph Nathane01ccf12015-04-16 10:40:32 -0700770 # This message is read by BrilloDeployOperation.
771 logging.notice('Copying %s to device.', pkg_name)
Ilja H. Friedel0ab63e12017-03-28 13:29:48 -0700772 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700773
David Pursell9476bf42015-03-30 13:34:27 -0700774 logging.info('Use portage temp dir %s', portage_tmpdir)
775
Ralph Nathane01ccf12015-04-16 10:40:32 -0700776 # This message is read by BrilloDeployOperation.
777 logging.notice('Installing %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700778 pkg_path = os.path.join(pkg_dir, pkg_name)
779
780 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
781 # chromeos-base packages will be skipped due to the configuration
782 # in /etc/protage/make.profile/package.provided. However, there is
783 # a known bug that /usr/local/etc/portage is not setup properly
784 # (crbug.com/312041). This does not affect `cros deploy` because
785 # we do not use the preset PKGDIR.
786 extra_env = {
787 'FEATURES': '-sandbox',
788 'PKGDIR': pkgroot,
789 'PORTAGE_CONFIGROOT': '/usr/local',
790 'PORTAGE_TMPDIR': portage_tmpdir,
791 'PORTDIR': device.work_dir,
792 'CONFIG_PROTECT': '-*',
793 }
Alex Klein9a1b3722020-01-28 09:59:35 -0700794 # --ignore-built-slot-operator-deps because we don't rebuild everything.
795 # It can cause errors, but that's expected with cros deploy since it's just a
796 # best effort to prevent developers avoid rebuilding an image every time.
797 cmd = ['emerge', '--ignore-built-slot-operator-deps', '--usepkg', pkg_path,
798 '--root=%s' % root]
David Pursell9476bf42015-03-30 13:34:27 -0700799 if extra_args:
800 cmd.append(extra_args)
801
Alex Klein9a1b3722020-01-28 09:59:35 -0700802 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
803 'packages built against the old version may not be able to '
804 'load the new .so. This is expected, and you will just need '
805 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700806 try:
Greg Kerrb96c02c2019-02-08 14:32:41 -0800807 result = device.RunCommand(cmd, extra_env=extra_env, remote_sudo=True,
808 capture_output=True, debug_level=logging.INFO)
809
810 pattern = ('A requested package will not be merged because '
811 'it is listed in package.provided')
812 output = result.error.replace('\n', ' ').replace('\r', '')
813 if pattern in output:
814 error = ('Package failed to emerge: %s\n'
815 'Remove %s from /etc/portage/make.profile/'
816 'package.provided/chromeos-base.packages\n'
817 '(also see crbug.com/920140 for more context)\n'
818 % (pattern, pkg_name))
819 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700820 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700821 logging.error('Failed to emerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700822 raise
823 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700824 logging.notice('%s has been installed.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700825
826
Qijiang Fand5958192019-07-26 12:32:36 +0900827def _RestoreSELinuxContext(device, pkgpath, root):
Qijiang Fan8a945032019-04-25 20:53:29 +0900828 """Restore SELinux context for files in a given pacakge.
829
830 This reads the tarball from pkgpath, and calls restorecon on device to
831 restore SELinux context for files listed in the tarball, assuming those files
832 are installed to /
833
834 Args:
835 device: a ChromiumOSDevice object
836 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900837 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900838 """
Ben Pastene5f03b052019-08-12 18:03:24 -0700839 enforced = device.IsSELinuxEnforced()
Qijiang Fan8a945032019-04-25 20:53:29 +0900840 if enforced:
841 device.RunCommand(['setenforce', '0'])
842 pkgroot = os.path.join(device.work_dir, 'packages')
843 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
844 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
845 # Testing shows restorecon splits on newlines instead of spaces.
846 device.RunCommand(
Qijiang Fand5958192019-07-26 12:32:36 +0900847 ['cd', root, '&&',
848 'tar', 'tf', pkgpath_device, '|',
849 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900850 remote_sudo=True)
851 if enforced:
852 device.RunCommand(['setenforce', '1'])
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900853
854
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700855def _GetPackagesByCPV(cpvs, strip, sysroot):
856 """Returns paths to binary packages corresponding to |cpvs|.
857
858 Args:
859 cpvs: List of CPV components given by portage_util.SplitCPV().
860 strip: True to run strip_package.
861 sysroot: Sysroot path.
862
863 Returns:
864 List of paths corresponding to |cpvs|.
865
866 Raises:
867 DeployError: If a package is missing.
868 """
869 packages_dir = None
870 if strip:
871 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400872 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700873 ['strip_package', '--sysroot', sysroot] +
Alex Klein7078e252018-10-02 10:21:04 -0600874 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700875 packages_dir = _STRIPPED_PACKAGES_DIR
876 except cros_build_lib.RunCommandError:
877 logging.error('Cannot strip packages %s',
878 ' '.join([str(cpv) for cpv in cpvs]))
879 raise
880
881 paths = []
882 for cpv in cpvs:
883 path = portage_util.GetBinaryPackagePath(
884 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
885 packages_dir=packages_dir)
886 if not path:
887 raise DeployError('Missing package %s.' % cpv)
888 paths.append(path)
889
890 return paths
891
892
893def _GetPackagesPaths(pkgs, strip, sysroot):
894 """Returns paths to binary |pkgs|.
895
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700896 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700897 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700898 strip: Whether or not to run strip_package for CPV packages.
899 sysroot: The sysroot path.
900
901 Returns:
902 List of paths corresponding to |pkgs|.
903 """
Ned Nguyend0db4072019-02-22 14:19:21 -0700904 cpvs = [portage_util.SplitCPV(p) for p in pkgs]
905 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700906
907
David Pursell9476bf42015-03-30 13:34:27 -0700908def _Unmerge(device, pkg, root):
909 """Unmerges |pkg| on |device|.
910
911 Args:
912 device: A RemoteDevice object.
913 pkg: A package name.
914 root: Package installation root path.
915 """
Ralph Nathane01ccf12015-04-16 10:40:32 -0700916 pkg_name = os.path.basename(pkg)
917 # This message is read by BrilloDeployOperation.
918 logging.notice('Unmerging %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700919 cmd = ['qmerge', '--yes']
920 # Check if qmerge is available on the device. If not, use emerge.
921 if device.RunCommand(
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500922 ['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700923 cmd = ['emerge']
924
925 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
926 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700927 # Always showing the emerge output for clarity.
David Pursell9476bf42015-03-30 13:34:27 -0700928 device.RunCommand(cmd, capture_output=False, remote_sudo=True,
929 debug_level=logging.INFO)
930 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700931 logging.error('Failed to unmerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700932 raise
933 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700934 logging.notice('%s has been uninstalled.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700935
936
937def _ConfirmDeploy(num_updates):
938 """Returns whether we can continue deployment."""
939 if num_updates > _MAX_UPDATES_NUM:
940 logging.warning(_MAX_UPDATES_WARNING)
941 return cros_build_lib.BooleanPrompt(default=False)
942
943 return True
944
945
Ralph Nathane01ccf12015-04-16 10:40:32 -0700946def _EmergePackages(pkgs, device, strip, sysroot, root, emerge_args):
947 """Call _Emerge for each packge in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700948 dlc_deployed = False
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700949 for pkg_path in _GetPackagesPaths(pkgs, strip, sysroot):
950 _Emerge(device, pkg_path, root, extra_args=emerge_args)
Ben Pastene5f03b052019-08-12 18:03:24 -0700951 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900952 _RestoreSELinuxContext(device, pkg_path, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700953 if _DeployDLCImage(device, pkg_path):
954 dlc_deployed = True
955
956 # Restart dlcservice so it picks up the newly installed DLC modules (in case
957 # we installed new DLC images).
958 if dlc_deployed:
959 device.RunCommand(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -0700960
961
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700962def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -0700963 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700964 dlc_uninstalled = False
Ralph Nathane01ccf12015-04-16 10:40:32 -0700965 for pkg in pkgs:
966 _Unmerge(device, pkg, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700967 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
968 dlc_uninstalled = True
969
970 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
971 # uninstalled DLC images).
972 if dlc_uninstalled:
973 device.RunCommand(['restart', 'dlcservice'])
974
975
976def _UninstallDLCImage(device, pkg_attrs):
977 """Uninstall a DLC image."""
978 if _DLC_ID in pkg_attrs:
979 dlc_id = pkg_attrs[_DLC_ID]
980 logging.notice('Uninstalling DLC image for %s', dlc_id)
981
982 device.RunCommand(['sudo', '-u', 'chronos', 'dlcservice_util',
983 '--uninstall', '--dlc_ids=%s' % dlc_id])
984 return True
985 else:
986 logging.debug('DLC_ID not found in package')
987 return False
988
989
990def _DeployDLCImage(device, pkg_path):
991 """Deploy (install and mount) a DLC image."""
992 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
993 if dlc_id and dlc_package:
994 logging.notice('Deploy a DLC image for %s', dlc_id)
995
996 dlc_path_src = os.path.join('/build/rootfs/dlc', dlc_id, dlc_package,
997 'dlc.img')
998 dlc_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
999 dlc_path_a = os.path.join(dlc_path, 'dlc_a')
1000 dlc_path_b = os.path.join(dlc_path, 'dlc_b')
1001 # Create folders for DLC images.
1002 device.RunCommand(['mkdir', '-p', dlc_path_a, dlc_path_b])
1003 # Copy images to the destination folders.
1004 device.RunCommand(['cp', dlc_path_src,
1005 os.path.join(dlc_path_a, 'dlc.img')])
1006 device.RunCommand(['cp', dlc_path_src,
1007 os.path.join(dlc_path_b, 'dlc.img')])
1008
1009 # Set the proper perms and ownership so dlcservice can access the image.
1010 device.RunCommand(['chmod', '-R', '0755', _DLC_INSTALL_ROOT])
1011 device.RunCommand(['chown', '-R', 'dlcservice:dlcservice',
1012 _DLC_INSTALL_ROOT])
1013 return True
1014 else:
1015 logging.debug('DLC_ID not found in package')
1016 return False
1017
1018
1019def _GetDLCInfo(device, pkg_path, from_dut):
1020 """Returns information of a DLC given its package path.
1021
1022 Args:
1023 device: commandline.Device object; None to use the default device.
1024 pkg_path: path to the package.
1025 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1026 info from host.
1027
1028 Returns:
1029 A tuple (dlc_id, dlc_package).
1030 """
1031 environment_content = ''
1032 if from_dut:
1033 # On DUT, |pkg_path| is the directory which contains environment file.
1034 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1035 result = device.RunCommand(['test', '-f', environment_path],
Woody Chowde57a322020-01-07 16:18:52 +09001036 check=False, encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001037 if result.returncode == 1:
1038 # The package is not installed on DUT yet. Skip extracting info.
1039 return None, None
Woody Chowde57a322020-01-07 16:18:52 +09001040 result = device.RunCommand(['bzip2', '-d', '-c', environment_path],
1041 encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001042 environment_content = result.output
1043 else:
1044 # On host, pkg_path is tbz2 file which contains environment file.
1045 # Extract the metadata of the package file.
1046 data = portage.xpak.tbz2(pkg_path).get_data()
1047 # Extract the environment metadata.
Woody Chowde57a322020-01-07 16:18:52 +09001048 environment_content = bz2.decompress(
Alex Klein9a1b3722020-01-28 09:59:35 -07001049 data[_ENVIRONMENT_FILENAME.encode('utf-8')])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001050
1051 with tempfile.NamedTemporaryFile() as f:
1052 # Dumps content into a file so we can use osutils.SourceEnvironment.
1053 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001054 osutils.WriteFile(path, environment_content, mode='wb')
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001055 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE))
1056 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001057
1058
Gilad Arnolda0a98062015-07-07 08:34:27 -07001059def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1060 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1061 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1062 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001063 """Deploys packages to a device.
1064
1065 Args:
David Pursell2e773382015-04-03 14:30:47 -07001066 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001067 packages: List of packages (strings) to deploy to device.
1068 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001069 emerge: True to emerge package, False to unmerge.
1070 update: Check installed version on device.
1071 deep: Install dependencies also. Implies |update|.
1072 deep_rev: Install reverse dependencies. Implies |deep|.
1073 clean_binpkg: Clean outdated binary packages.
1074 root: Package installation root path.
1075 strip: Run strip_package to filter out preset paths in the package.
1076 emerge_args: Extra arguments to pass to emerge.
1077 ssh_private_key: Path to an SSH private key file; None to use test keys.
1078 ping: True to ping the device before trying to connect.
1079 force: Ignore sanity checks and prompts.
1080 dry_run: Print deployment plan but do not deploy anything.
1081
1082 Raises:
1083 ValueError: Invalid parameter or parameter combination.
1084 DeployError: Unrecoverable failure during deploy.
1085 """
1086 if deep_rev:
1087 deep = True
1088 if deep:
1089 update = True
1090
Gilad Arnolda0a98062015-07-07 08:34:27 -07001091 if not packages:
1092 raise DeployError('No packages provided, nothing to deploy.')
1093
David Pursell9476bf42015-03-30 13:34:27 -07001094 if update and not emerge:
1095 raise ValueError('Cannot update and unmerge.')
1096
David Pursell2e773382015-04-03 14:30:47 -07001097 if device:
1098 hostname, username, port = device.hostname, device.username, device.port
1099 else:
1100 hostname, username, port = None, None, None
1101
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001102 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001103 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001104 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001105 # Somewhat confusing to clobber, but here we are.
1106 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001107 with remote_access.ChromiumOSDeviceHandler(
1108 hostname, port=port, username=username, private_key=ssh_private_key,
1109 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001110 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001111
Gilad Arnolda0a98062015-07-07 08:34:27 -07001112 board = cros_build_lib.GetBoard(device_board=device.board,
1113 override_board=board)
1114 if not force and board != device.board:
1115 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001116 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001117
Gilad Arnolda0a98062015-07-07 08:34:27 -07001118 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -07001119
1120 if clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001121 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001122 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001123
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001124 # Remount rootfs as writable if necessary.
1125 if not device.MountRootfsReadWrite():
1126 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001127
1128 # Obtain list of packages to upgrade/remove.
1129 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001130 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001131 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001132 if emerge:
1133 action_str = 'emerge'
1134 else:
1135 pkgs.reverse()
1136 action_str = 'unmerge'
1137
1138 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001139 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001140 return
1141
Ralph Nathane01ccf12015-04-16 10:40:32 -07001142 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001143 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001144 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001145
1146 if dry_run or not _ConfirmDeploy(num_updates):
1147 return
1148
Ralph Nathane01ccf12015-04-16 10:40:32 -07001149 # Select function (emerge or unmerge) and bind args.
1150 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001151 func = functools.partial(_EmergePackages, pkgs, device, strip,
Ralph Nathane01ccf12015-04-16 10:40:32 -07001152 sysroot, root, emerge_args)
1153 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001154 func = functools.partial(_UnmergePackages, pkgs, device, root,
1155 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001156
1157 # Call the function with the progress bar or with normal output.
1158 if command.UseProgressBar():
1159 op = BrilloDeployOperation(len(pkgs), emerge)
1160 op.Run(func, log_level=logging.DEBUG)
1161 else:
1162 func()
David Pursell9476bf42015-03-30 13:34:27 -07001163
Ben Pastene5f03b052019-08-12 18:03:24 -07001164 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001165 if sum(x.count('selinux-policy') for x in pkgs):
1166 logging.warning(
1167 'Deploying SELinux policy will not take effect until reboot. '
1168 'SELinux policy is loaded by init. Also, security contexts '
1169 '(labels) in files will require manual relabeling by the user '
1170 'if your policy modifies the file contexts.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001171
David Pursell9476bf42015-03-30 13:34:27 -07001172 logging.warning('Please restart any updated services on the device, '
1173 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001174 except Exception:
1175 if lsb_release:
1176 lsb_entries = sorted(lsb_release.items())
1177 logging.info('Following are the LSB version details of the device:\n%s',
1178 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1179 raise