blob: 5e03429c0a1cd89d8808b5e631ad826c92a273be [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
Alex Kleinaaddc932020-01-30 15:02:24 -07006"""Deploy packages onto a target device.
7
8Integration tests for this file can be found at cli/cros/tests/cros_vm_tests.py.
9See that file for more information.
10"""
David Pursell9476bf42015-03-30 13:34:27 -070011
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040012from __future__ import division
David Pursell9476bf42015-03-30 13:34:27 -070013from __future__ import print_function
14
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070015import bz2
David Pursell9476bf42015-03-30 13:34:27 -070016import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070017import functools
David Pursell9476bf42015-03-30 13:34:27 -070018import json
19import os
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070020import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070021
Ralph Nathane01ccf12015-04-16 10:40:32 -070022from chromite.cli import command
David Pursell9476bf42015-03-30 13:34:27 -070023from chromite.lib import cros_build_lib
24from chromite.lib import cros_logging as logging
Ralph Nathane01ccf12015-04-16 10:40:32 -070025from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070026from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070027from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070028from chromite.lib import remote_access
29try:
30 import portage
31except ImportError:
32 if cros_build_lib.IsInsideChroot():
33 raise
34
35
36_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
37# This is defined in src/platform/dev/builder.py
38_STRIPPED_PACKAGES_DIR = 'stripped-packages'
39
40_MAX_UPDATES_NUM = 10
41_MAX_UPDATES_WARNING = (
42 'You are about to update a large number of installed packages, which '
43 'might take a long time, fail midway, or leave the target in an '
44 'inconsistent state. It is highly recommended that you flash a new image '
45 'instead.')
46
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070047_DLC_ID = 'DLC_ID'
48_DLC_PACKAGE = 'DLC_PACKAGE'
49_ENVIRONMENT_FILENAME = 'environment.bz2'
50_DLC_INSTALL_ROOT = '/var/cache/dlc'
51
David Pursell9476bf42015-03-30 13:34:27 -070052
53class DeployError(Exception):
54 """Thrown when an unrecoverable error is encountered during deploy."""
55
56
Ralph Nathane01ccf12015-04-16 10:40:32 -070057class BrilloDeployOperation(operation.ProgressBarOperation):
58 """ProgressBarOperation specific for brillo deploy."""
Alex Kleinaaddc932020-01-30 15:02:24 -070059 # These two variables are used to validate the output in the VM integration
60 # tests. Changes to the output must be reflected here.
61 MERGE_EVENTS = ['NOTICE: Copying', 'NOTICE: Installing', 'WARNING: Ignoring',
Achuith Bhandarkar0487c312019-04-22 12:19:25 -070062 'emerge --usepkg', 'has been installed.']
Ralph Nathan90475a12015-05-20 13:19:01 -070063 UNMERGE_EVENTS = ['NOTICE: Unmerging', 'has been uninstalled.']
Ralph Nathane01ccf12015-04-16 10:40:32 -070064
65 def __init__(self, pkg_count, emerge):
66 """Construct BrilloDeployOperation object.
67
68 Args:
69 pkg_count: number of packages being built.
70 emerge: True if emerge, False is unmerge.
71 """
72 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070073 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070074 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070075 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070076 self._events = self.UNMERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070077 self._total = pkg_count * len(self._events)
78 self._completed = 0
79
Ralph Nathandc14ed92015-04-22 11:17:40 -070080 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070081 """Parse the output of brillo deploy to update a progress bar."""
82 stdout = self._stdout.read()
83 stderr = self._stderr.read()
84 output = stdout + stderr
85 for event in self._events:
86 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040087 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -070088
89
David Pursell9476bf42015-03-30 13:34:27 -070090class _InstallPackageScanner(object):
91 """Finds packages that need to be installed on a target device.
92
93 Scans the sysroot bintree, beginning with a user-provided list of packages,
94 to find all packages that need to be installed. If so instructed,
95 transitively scans forward (mandatory) and backward (optional) dependencies
96 as well. A package will be installed if missing on the target (mandatory
97 packages only), or it will be updated if its sysroot version and build time
98 are different from the target. Common usage:
99
100 pkg_scanner = _InstallPackageScanner(sysroot)
101 pkgs = pkg_scanner.Run(...)
102 """
103
104 class VartreeError(Exception):
105 """An error in the processing of the installed packages tree."""
106
107 class BintreeError(Exception):
108 """An error in the processing of the source binpkgs tree."""
109
110 class PkgInfo(object):
111 """A record containing package information."""
112
113 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
114
115 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
116 self.cpv = cpv
117 self.build_time = build_time
118 self.rdeps_raw = rdeps_raw
119 self.rdeps = set() if rdeps is None else rdeps
120 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
121
122 # Python snippet for dumping vartree info on the target. Instantiate using
123 # _GetVartreeSnippet().
124 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700125import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700126import os
127import portage
128
129# Normalize the path to match what portage will index.
130target_root = os.path.normpath('%(root)s')
131if not target_root.endswith('/'):
132 target_root += '/'
133trees = portage.create_trees(target_root=target_root, config_root='/')
134vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700135pkg_info = []
136for cpv in vartree.dbapi.cpv_all():
137 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
138 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
139 pkg_info.append((cpv, slot, rdep_raw, build_time))
140
141print(json.dumps(pkg_info))
142"""
143
144 def __init__(self, sysroot):
145 self.sysroot = sysroot
146 # Members containing the sysroot (binpkg) and target (installed) package DB.
147 self.target_db = None
148 self.binpkgs_db = None
149 # Members for managing the dependency resolution work queue.
150 self.queue = None
151 self.seen = None
152 self.listed = None
153
154 @staticmethod
155 def _GetCP(cpv):
156 """Returns the CP value for a given CPV string."""
157 attrs = portage_util.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600158 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700159 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600160 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700161
162 @staticmethod
163 def _InDB(cp, slot, db):
164 """Returns whether CP and slot are found in a database (if provided)."""
165 cp_slots = db.get(cp) if db else None
166 return cp_slots is not None and (not slot or slot in cp_slots)
167
168 @staticmethod
169 def _AtomStr(cp, slot):
170 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
171 return '%s:%s' % (cp, slot) if slot else cp
172
173 @classmethod
174 def _GetVartreeSnippet(cls, root='/'):
175 """Returns a code snippet for dumping the vartree on the target.
176
177 Args:
178 root: The installation root.
179
180 Returns:
181 The said code snippet (string) with parameters filled in.
182 """
183 return cls._GET_VARTREE % {'root': root}
184
185 @classmethod
186 def _StripDepAtom(cls, dep_atom, installed_db=None):
187 """Strips a dependency atom and returns a (CP, slot) pair."""
188 # TODO(garnold) This is a gross simplification of ebuild dependency
189 # semantics, stripping and ignoring various qualifiers (versions, slots,
190 # USE flag, negation) and will likely need to be fixed. chromium:447366.
191
192 # Ignore unversioned blockers, leaving them for the user to resolve.
193 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
194 return None, None
195
196 cp = dep_atom
197 slot = None
198 require_installed = False
199
200 # Versioned blockers should be updated, but only if already installed.
201 # These are often used for forcing cascaded updates of multiple packages,
202 # so we're treating them as ordinary constraints with hopes that it'll lead
203 # to the desired result.
204 if cp.startswith('!'):
205 cp = cp.lstrip('!')
206 require_installed = True
207
208 # Remove USE flags.
209 if '[' in cp:
210 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
211
212 # Separate the slot qualifier and strip off subslots.
213 if ':' in cp:
214 cp, slot = cp.split(':')
215 for delim in ('/', '='):
216 slot = slot.split(delim, 1)[0]
217
218 # Strip version wildcards (right), comparators (left).
219 cp = cp.rstrip('*')
220 cp = cp.lstrip('<=>~')
221
222 # Turn into CP form.
223 cp = cls._GetCP(cp)
224
225 if require_installed and not cls._InDB(cp, None, installed_db):
226 return None, None
227
228 return cp, slot
229
230 @classmethod
231 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
232 """Resolves and returns a list of dependencies from a dependency string.
233
234 This parses a dependency string and returns a list of package names and
235 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
236 resolving disjunctive deps, we include all choices that are fully present
237 in |installed_db|. If none is present, we choose an arbitrary one that is
238 available.
239
240 Args:
241 dep_str: A raw dependency string.
242 installed_db: A database of installed packages.
243 avail_db: A database of packages available for installation.
244
245 Returns:
246 A list of pairs (CP, slot).
247
248 Raises:
249 ValueError: the dependencies string is malformed.
250 """
251 def ProcessSubDeps(dep_exp, disjunct):
252 """Parses and processes a dependency (sub)expression."""
253 deps = set()
254 default_deps = set()
255 sub_disjunct = False
256 for dep_sub_exp in dep_exp:
257 sub_deps = set()
258
259 if isinstance(dep_sub_exp, (list, tuple)):
260 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
261 sub_disjunct = False
262 elif sub_disjunct:
263 raise ValueError('Malformed disjunctive operation in deps')
264 elif dep_sub_exp == '||':
265 sub_disjunct = True
266 elif dep_sub_exp.endswith('?'):
267 raise ValueError('Dependencies contain a conditional')
268 else:
269 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
270 if cp:
271 sub_deps = set([(cp, slot)])
272 elif disjunct:
273 raise ValueError('Atom in disjunct ignored')
274
275 # Handle sub-deps of a disjunctive expression.
276 if disjunct:
277 # Make the first available choice the default, for use in case that
278 # no option is installed.
279 if (not default_deps and avail_db is not None and
280 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
281 default_deps = sub_deps
282
283 # If not all sub-deps are installed, then don't consider them.
284 if not all([cls._InDB(cp, slot, installed_db)
285 for cp, slot in sub_deps]):
286 sub_deps = set()
287
288 deps.update(sub_deps)
289
290 return deps or default_deps
291
292 try:
293 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
294 except portage.exception.InvalidDependString as e:
295 raise ValueError('Invalid dep string: %s' % e)
296 except ValueError as e:
297 raise ValueError('%s: %s' % (e, dep_str))
298
299 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
300 installed_db=None):
301 """Returns a database of packages given a list of CPV info.
302
303 Args:
304 cpv_info: A list of tuples containing package CPV and attributes.
305 process_rdeps: Whether to populate forward dependencies.
306 process_rev_rdeps: Whether to populate reverse dependencies.
307 installed_db: A database of installed packages for filtering disjunctive
308 choices against; if None, using own built database.
309
310 Returns:
311 A map from CP values to another dictionary that maps slots to package
312 attribute tuples. Tuples contain a CPV value (string), build time
313 (string), runtime dependencies (set), and reverse dependencies (set,
314 empty if not populated).
315
316 Raises:
317 ValueError: If more than one CPV occupies a single slot.
318 """
319 db = {}
320 logging.debug('Populating package DB...')
321 for cpv, slot, rdeps_raw, build_time in cpv_info:
322 cp = self._GetCP(cpv)
323 cp_slots = db.setdefault(cp, dict())
324 if slot in cp_slots:
325 raise ValueError('More than one package found for %s' %
326 self._AtomStr(cp, slot))
327 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
328 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
329 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
330
331 avail_db = db
332 if installed_db is None:
333 installed_db = db
334 avail_db = None
335
336 # Add approximate forward dependencies.
337 if process_rdeps:
338 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400339 for cp, cp_slots in db.items():
340 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700341 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
342 installed_db, avail_db))
343 logging.debug(' %s (%s) processed rdeps: %s',
344 self._AtomStr(cp, slot), pkg_info.cpv,
345 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
346 for rdep_cp, rdep_slot in pkg_info.rdeps]))
347
348 # Add approximate reverse dependencies (optional).
349 if process_rev_rdeps:
350 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400351 for cp, cp_slots in db.items():
352 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700353 for rdep_cp, rdep_slot in pkg_info.rdeps:
354 to_slots = db.get(rdep_cp)
355 if not to_slots:
356 continue
357
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400358 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700359 if rdep_slot and to_slot != rdep_slot:
360 continue
361 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
362 self._AtomStr(cp, slot), pkg_info.cpv,
363 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
364 to_pkg_info.rev_rdeps.add((cp, slot))
365
366 return db
367
368 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
369 """Initializes a dictionary of packages installed on |device|."""
370 get_vartree_script = self._GetVartreeSnippet(root)
371 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400372 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700373 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700374 except cros_build_lib.RunCommandError as e:
375 logging.error('Cannot get target vartree:\n%s', e.result.error)
376 raise
377
378 try:
379 self.target_db = self._BuildDB(json.loads(result.output),
380 process_rdeps, process_rev_rdeps)
381 except ValueError as e:
382 raise self.VartreeError(str(e))
383
384 def _InitBinpkgDB(self, process_rdeps):
385 """Initializes a dictionary of binary packages for updating the target."""
386 # Get build root trees; portage indexes require a trailing '/'.
387 build_root = os.path.join(self.sysroot, '')
388 trees = portage.create_trees(target_root=build_root, config_root=build_root)
389 bintree = trees[build_root]['bintree']
390 binpkgs_info = []
391 for cpv in bintree.dbapi.cpv_all():
392 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
393 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
394 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
395
396 try:
397 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
398 installed_db=self.target_db)
399 except ValueError as e:
400 raise self.BintreeError(str(e))
401
402 def _InitDepQueue(self):
403 """Initializes the dependency work queue."""
404 self.queue = set()
405 self.seen = {}
406 self.listed = set()
407
408 def _EnqDep(self, dep, listed, optional):
409 """Enqueues a dependency if not seen before or if turned non-optional."""
410 if dep in self.seen and (optional or not self.seen[dep]):
411 return False
412
413 self.queue.add(dep)
414 self.seen[dep] = optional
415 if listed:
416 self.listed.add(dep)
417 return True
418
419 def _DeqDep(self):
420 """Dequeues and returns a dependency, its listed and optional flags.
421
422 This returns listed packages first, if any are present, to ensure that we
423 correctly mark them as such when they are first being processed.
424 """
425 if self.listed:
426 dep = self.listed.pop()
427 self.queue.remove(dep)
428 listed = True
429 else:
430 dep = self.queue.pop()
431 listed = False
432
433 return dep, listed, self.seen[dep]
434
435 def _FindPackageMatches(self, cpv_pattern):
436 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
437
438 This is breaking |cpv_pattern| into its C, P and V components, each of
439 which may or may not be present or contain wildcards. It then scans the
440 binpkgs database to find all atoms that match these components, returning a
441 list of CP and slot qualifier. When the pattern does not specify a version,
442 or when a CP has only one slot in the binpkgs database, we omit the slot
443 qualifier in the result.
444
445 Args:
446 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
447
448 Returns:
449 A list of (CPV, slot) pairs of packages in the binpkgs database that
450 match the pattern.
451 """
452 attrs = portage_util.SplitCPV(cpv_pattern, strict=False)
453 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
454 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400455 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700456 if not fnmatch.fnmatchcase(cp, cp_pattern):
457 continue
458
459 # If no version attribute was given or there's only one slot, omit the
460 # slot qualifier.
461 if not attrs.version or len(cp_slots) == 1:
462 matches.append((cp, None))
463 else:
464 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400465 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700466 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
467 matches.append((cp, slot))
468
469 return matches
470
471 def _FindPackage(self, pkg):
472 """Returns the (CP, slot) pair for a package matching |pkg|.
473
474 Args:
475 pkg: Path to a binary package or a (partial) package CPV specifier.
476
477 Returns:
478 A (CP, slot) pair for the given package; slot may be None (unspecified).
479
480 Raises:
481 ValueError: if |pkg| is not a binpkg file nor does it match something
482 that's in the bintree.
483 """
484 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
485 package = os.path.basename(os.path.splitext(pkg)[0])
486 category = os.path.basename(os.path.dirname(pkg))
487 return self._GetCP(os.path.join(category, package)), None
488
489 matches = self._FindPackageMatches(pkg)
490 if not matches:
491 raise ValueError('No package found for %s' % pkg)
492
493 idx = 0
494 if len(matches) > 1:
495 # Ask user to pick among multiple matches.
496 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
497 ['%s:%s' % (cp, slot) if slot else cp
498 for cp, slot in matches])
499
500 return matches[idx]
501
502 def _NeedsInstall(self, cpv, slot, build_time, optional):
503 """Returns whether a package needs to be installed on the target.
504
505 Args:
506 cpv: Fully qualified CPV (string) of the package.
507 slot: Slot identifier (string).
508 build_time: The BUILT_TIME value (string) of the binpkg.
509 optional: Whether package is optional on the target.
510
511 Returns:
512 A tuple (install, update) indicating whether to |install| the package and
513 whether it is an |update| to an existing package.
514
515 Raises:
516 ValueError: if slot is not provided.
517 """
518 # If not checking installed packages, always install.
519 if not self.target_db:
520 return True, False
521
522 cp = self._GetCP(cpv)
523 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
524 if target_pkg_info is not None:
525 if cpv != target_pkg_info.cpv:
526 attrs = portage_util.SplitCPV(cpv)
527 target_attrs = portage_util.SplitCPV(target_pkg_info.cpv)
528 logging.debug('Updating %s: version (%s) different on target (%s)',
529 cp, attrs.version, target_attrs.version)
530 return True, True
531
532 if build_time != target_pkg_info.build_time:
533 logging.debug('Updating %s: build time (%s) different on target (%s)',
534 cpv, build_time, target_pkg_info.build_time)
535 return True, True
536
537 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
538 cp, target_pkg_info.cpv, target_pkg_info.build_time)
539 return False, False
540
541 if optional:
542 logging.debug('Not installing %s: missing on target but optional', cp)
543 return False, False
544
545 logging.debug('Installing %s: missing on target and non-optional (%s)',
546 cp, cpv)
547 return True, False
548
549 def _ProcessDeps(self, deps, reverse):
550 """Enqueues dependencies for processing.
551
552 Args:
553 deps: List of dependencies to enqueue.
554 reverse: Whether these are reverse dependencies.
555 """
556 if not deps:
557 return
558
559 logging.debug('Processing %d %s dep(s)...', len(deps),
560 'reverse' if reverse else 'forward')
561 num_already_seen = 0
562 for dep in deps:
563 if self._EnqDep(dep, False, reverse):
564 logging.debug(' Queued dep %s', dep)
565 else:
566 num_already_seen += 1
567
568 if num_already_seen:
569 logging.debug('%d dep(s) already seen', num_already_seen)
570
571 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
572 """Returns a dictionary of packages that need to be installed on the target.
573
574 Args:
575 process_rdeps: Whether to trace forward dependencies.
576 process_rev_rdeps: Whether to trace backward dependencies as well.
577
578 Returns:
579 A dictionary mapping CP values (string) to tuples containing a CPV
580 (string), a slot (string), a boolean indicating whether the package
581 was initially listed in the queue, and a boolean indicating whether this
582 is an update to an existing package.
583 """
584 installs = {}
585 while self.queue:
586 dep, listed, optional = self._DeqDep()
587 cp, required_slot = dep
588 if cp in installs:
589 logging.debug('Already updating %s', cp)
590 continue
591
592 cp_slots = self.binpkgs_db.get(cp, dict())
593 logging.debug('Checking packages matching %s%s%s...', cp,
594 ' (slot: %s)' % required_slot if required_slot else '',
595 ' (optional)' if optional else '')
596 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400597 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700598 if required_slot and slot != required_slot:
599 continue
600
601 num_processed += 1
602 logging.debug(' Checking %s...', pkg_info.cpv)
603
604 install, update = self._NeedsInstall(pkg_info.cpv, slot,
605 pkg_info.build_time, optional)
606 if not install:
607 continue
608
609 installs[cp] = (pkg_info.cpv, slot, listed, update)
610
611 # Add forward and backward runtime dependencies to queue.
612 if process_rdeps:
613 self._ProcessDeps(pkg_info.rdeps, False)
614 if process_rev_rdeps:
615 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
616 if target_pkg_info:
617 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
618
619 if num_processed == 0:
620 logging.warning('No qualified bintree package corresponding to %s', cp)
621
622 return installs
623
624 def _SortInstalls(self, installs):
625 """Returns a sorted list of packages to install.
626
627 Performs a topological sort based on dependencies found in the binary
628 package database.
629
630 Args:
631 installs: Dictionary of packages to install indexed by CP.
632
633 Returns:
634 A list of package CPVs (string).
635
636 Raises:
637 ValueError: If dependency graph contains a cycle.
638 """
639 not_visited = set(installs.keys())
640 curr_path = []
641 sorted_installs = []
642
643 def SortFrom(cp):
644 """Traverses dependencies recursively, emitting nodes in reverse order."""
645 cpv, slot, _, _ = installs[cp]
646 if cpv in curr_path:
647 raise ValueError('Dependencies contain a cycle: %s -> %s' %
648 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
649 curr_path.append(cpv)
650 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
651 if rdep_cp in not_visited:
652 not_visited.remove(rdep_cp)
653 SortFrom(rdep_cp)
654
655 sorted_installs.append(cpv)
656 curr_path.pop()
657
658 # So long as there's more packages, keep expanding dependency paths.
659 while not_visited:
660 SortFrom(not_visited.pop())
661
662 return sorted_installs
663
664 def _EnqListedPkg(self, pkg):
665 """Finds and enqueues a listed package."""
666 cp, slot = self._FindPackage(pkg)
667 if cp not in self.binpkgs_db:
668 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
669 self._EnqDep((cp, slot), True, False)
670
671 def _EnqInstalledPkgs(self):
672 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400673 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700674 target_cp_slots = self.target_db.get(cp)
675 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400676 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700677 if slot in target_cp_slots:
678 self._EnqDep((cp, slot), True, False)
679
680 def Run(self, device, root, listed_pkgs, update, process_rdeps,
681 process_rev_rdeps):
682 """Computes the list of packages that need to be installed on a target.
683
684 Args:
685 device: Target handler object.
686 root: Package installation root.
687 listed_pkgs: Package names/files listed by the user.
688 update: Whether to read the target's installed package database.
689 process_rdeps: Whether to trace forward dependencies.
690 process_rev_rdeps: Whether to trace backward dependencies as well.
691
692 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700693 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
694 list of package CPVs (string) to install on the target in an order that
695 satisfies their inter-dependencies, |listed| the subset that was
696 requested by the user, and |num_updates| the number of packages being
697 installed over preexisting versions. Note that installation order should
698 be reversed for removal, |install_attrs| is a dictionary mapping a package
699 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700700 """
701 if process_rev_rdeps and not process_rdeps:
702 raise ValueError('Must processing forward deps when processing rev deps')
703 if process_rdeps and not update:
704 raise ValueError('Must check installed packages when processing deps')
705
706 if update:
707 logging.info('Initializing target intalled packages database...')
708 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
709
710 logging.info('Initializing binary packages database...')
711 self._InitBinpkgDB(process_rdeps)
712
713 logging.info('Finding listed package(s)...')
714 self._InitDepQueue()
715 for pkg in listed_pkgs:
716 if pkg == '@installed':
717 if not update:
718 raise ValueError(
719 'Must check installed packages when updating all of them.')
720 self._EnqInstalledPkgs()
721 else:
722 self._EnqListedPkg(pkg)
723
724 logging.info('Computing set of packages to install...')
725 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
726
727 num_updates = 0
728 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400729 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700730 if listed:
731 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400732 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700733 num_updates += 1
734
735 logging.info('Processed %d package(s), %d will be installed, %d are '
736 'updating existing packages',
737 len(self.seen), len(installs), num_updates)
738
739 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700740
741 install_attrs = {}
742 for pkg in sorted_installs:
743 pkg_path = os.path.join(root, portage.VDB_PATH, pkg)
744 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
745 install_attrs[pkg] = {}
746 if dlc_id and dlc_package:
747 install_attrs[pkg][_DLC_ID] = dlc_id
748
749 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700750
751
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700752def _Emerge(device, pkg_path, root, extra_args=None):
David Pursell9476bf42015-03-30 13:34:27 -0700753 """Copies |pkg| to |device| and emerges it.
754
755 Args:
756 device: A ChromiumOSDevice object.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700757 pkg_path: A path to a binary package.
David Pursell9476bf42015-03-30 13:34:27 -0700758 root: Package installation root path.
759 extra_args: Extra arguments to pass to emerge.
760
761 Raises:
762 DeployError: Unrecoverable error during emerge.
763 """
David Pursell9476bf42015-03-30 13:34:27 -0700764 pkgroot = os.path.join(device.work_dir, 'packages')
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700765 pkg_name = os.path.basename(pkg_path)
766 pkg_dirname = os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700767 pkg_dir = os.path.join(pkgroot, pkg_dirname)
Mike Frysinger15a4e012015-05-21 22:18:45 -0400768 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
769 # Clean out the dirs first if we had a previous emerge on the device so as to
770 # free up space for this emerge. The last emerge gets implicitly cleaned up
771 # when the device connection deletes its work_dir.
772 device.RunCommand(
773 ['rm', '-rf', pkg_dir, portage_tmpdir, '&&',
774 'mkdir', '-p', pkg_dir, portage_tmpdir], remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700775
Ralph Nathane01ccf12015-04-16 10:40:32 -0700776 # This message is read by BrilloDeployOperation.
777 logging.notice('Copying %s to device.', pkg_name)
Ilja H. Friedel0ab63e12017-03-28 13:29:48 -0700778 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700779
David Pursell9476bf42015-03-30 13:34:27 -0700780 logging.info('Use portage temp dir %s', portage_tmpdir)
781
Ralph Nathane01ccf12015-04-16 10:40:32 -0700782 # This message is read by BrilloDeployOperation.
783 logging.notice('Installing %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700784 pkg_path = os.path.join(pkg_dir, pkg_name)
785
786 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
787 # chromeos-base packages will be skipped due to the configuration
788 # in /etc/protage/make.profile/package.provided. However, there is
789 # a known bug that /usr/local/etc/portage is not setup properly
790 # (crbug.com/312041). This does not affect `cros deploy` because
791 # we do not use the preset PKGDIR.
792 extra_env = {
793 'FEATURES': '-sandbox',
794 'PKGDIR': pkgroot,
795 'PORTAGE_CONFIGROOT': '/usr/local',
796 'PORTAGE_TMPDIR': portage_tmpdir,
797 'PORTDIR': device.work_dir,
798 'CONFIG_PROTECT': '-*',
799 }
Alex Kleinaaddc932020-01-30 15:02:24 -0700800 # --ignore-built-slot-operator-deps because we don't rebuild everything.
801 # It can cause errors, but that's expected with cros deploy since it's just a
802 # best effort to prevent developers avoid rebuilding an image every time.
803 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', pkg_path,
804 '--root=%s' % root]
David Pursell9476bf42015-03-30 13:34:27 -0700805 if extra_args:
806 cmd.append(extra_args)
807
Alex Kleinaaddc932020-01-30 15:02:24 -0700808 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
809 'packages built against the old version may not be able to '
810 'load the new .so. This is expected, and you will just need '
811 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700812 try:
Greg Kerrb96c02c2019-02-08 14:32:41 -0800813 result = device.RunCommand(cmd, extra_env=extra_env, remote_sudo=True,
814 capture_output=True, debug_level=logging.INFO)
815
816 pattern = ('A requested package will not be merged because '
817 'it is listed in package.provided')
818 output = result.error.replace('\n', ' ').replace('\r', '')
819 if pattern in output:
820 error = ('Package failed to emerge: %s\n'
821 'Remove %s from /etc/portage/make.profile/'
822 'package.provided/chromeos-base.packages\n'
823 '(also see crbug.com/920140 for more context)\n'
824 % (pattern, pkg_name))
825 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700826 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700827 logging.error('Failed to emerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700828 raise
829 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700830 logging.notice('%s has been installed.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700831
832
Qijiang Fand5958192019-07-26 12:32:36 +0900833def _RestoreSELinuxContext(device, pkgpath, root):
Qijiang Fan8a945032019-04-25 20:53:29 +0900834 """Restore SELinux context for files in a given pacakge.
835
836 This reads the tarball from pkgpath, and calls restorecon on device to
837 restore SELinux context for files listed in the tarball, assuming those files
838 are installed to /
839
840 Args:
841 device: a ChromiumOSDevice object
842 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900843 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900844 """
Ben Pastene5f03b052019-08-12 18:03:24 -0700845 enforced = device.IsSELinuxEnforced()
Qijiang Fan8a945032019-04-25 20:53:29 +0900846 if enforced:
847 device.RunCommand(['setenforce', '0'])
848 pkgroot = os.path.join(device.work_dir, 'packages')
849 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
850 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
851 # Testing shows restorecon splits on newlines instead of spaces.
852 device.RunCommand(
Qijiang Fand5958192019-07-26 12:32:36 +0900853 ['cd', root, '&&',
854 'tar', 'tf', pkgpath_device, '|',
855 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900856 remote_sudo=True)
857 if enforced:
858 device.RunCommand(['setenforce', '1'])
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900859
860
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700861def _GetPackagesByCPV(cpvs, strip, sysroot):
862 """Returns paths to binary packages corresponding to |cpvs|.
863
864 Args:
865 cpvs: List of CPV components given by portage_util.SplitCPV().
866 strip: True to run strip_package.
867 sysroot: Sysroot path.
868
869 Returns:
870 List of paths corresponding to |cpvs|.
871
872 Raises:
873 DeployError: If a package is missing.
874 """
875 packages_dir = None
876 if strip:
877 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400878 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700879 ['strip_package', '--sysroot', sysroot] +
Alex Klein7078e252018-10-02 10:21:04 -0600880 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700881 packages_dir = _STRIPPED_PACKAGES_DIR
882 except cros_build_lib.RunCommandError:
883 logging.error('Cannot strip packages %s',
884 ' '.join([str(cpv) for cpv in cpvs]))
885 raise
886
887 paths = []
888 for cpv in cpvs:
889 path = portage_util.GetBinaryPackagePath(
890 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
891 packages_dir=packages_dir)
892 if not path:
893 raise DeployError('Missing package %s.' % cpv)
894 paths.append(path)
895
896 return paths
897
898
899def _GetPackagesPaths(pkgs, strip, sysroot):
900 """Returns paths to binary |pkgs|.
901
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700902 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700903 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700904 strip: Whether or not to run strip_package for CPV packages.
905 sysroot: The sysroot path.
906
907 Returns:
908 List of paths corresponding to |pkgs|.
909 """
Ned Nguyend0db4072019-02-22 14:19:21 -0700910 cpvs = [portage_util.SplitCPV(p) for p in pkgs]
911 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700912
913
David Pursell9476bf42015-03-30 13:34:27 -0700914def _Unmerge(device, pkg, root):
915 """Unmerges |pkg| on |device|.
916
917 Args:
918 device: A RemoteDevice object.
919 pkg: A package name.
920 root: Package installation root path.
921 """
Ralph Nathane01ccf12015-04-16 10:40:32 -0700922 pkg_name = os.path.basename(pkg)
923 # This message is read by BrilloDeployOperation.
924 logging.notice('Unmerging %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700925 cmd = ['qmerge', '--yes']
926 # Check if qmerge is available on the device. If not, use emerge.
927 if device.RunCommand(
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500928 ['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700929 cmd = ['emerge']
930
931 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
932 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700933 # Always showing the emerge output for clarity.
David Pursell9476bf42015-03-30 13:34:27 -0700934 device.RunCommand(cmd, capture_output=False, remote_sudo=True,
935 debug_level=logging.INFO)
936 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700937 logging.error('Failed to unmerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700938 raise
939 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700940 logging.notice('%s has been uninstalled.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700941
942
943def _ConfirmDeploy(num_updates):
944 """Returns whether we can continue deployment."""
945 if num_updates > _MAX_UPDATES_NUM:
946 logging.warning(_MAX_UPDATES_WARNING)
947 return cros_build_lib.BooleanPrompt(default=False)
948
949 return True
950
951
Ralph Nathane01ccf12015-04-16 10:40:32 -0700952def _EmergePackages(pkgs, device, strip, sysroot, root, emerge_args):
953 """Call _Emerge for each packge in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700954 dlc_deployed = False
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700955 for pkg_path in _GetPackagesPaths(pkgs, strip, sysroot):
956 _Emerge(device, pkg_path, root, extra_args=emerge_args)
Ben Pastene5f03b052019-08-12 18:03:24 -0700957 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900958 _RestoreSELinuxContext(device, pkg_path, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700959 if _DeployDLCImage(device, pkg_path):
960 dlc_deployed = True
961
962 # Restart dlcservice so it picks up the newly installed DLC modules (in case
963 # we installed new DLC images).
964 if dlc_deployed:
965 device.RunCommand(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -0700966
967
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700968def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -0700969 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700970 dlc_uninstalled = False
Ralph Nathane01ccf12015-04-16 10:40:32 -0700971 for pkg in pkgs:
972 _Unmerge(device, pkg, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700973 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
974 dlc_uninstalled = True
975
976 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
977 # uninstalled DLC images).
978 if dlc_uninstalled:
979 device.RunCommand(['restart', 'dlcservice'])
980
981
982def _UninstallDLCImage(device, pkg_attrs):
983 """Uninstall a DLC image."""
984 if _DLC_ID in pkg_attrs:
985 dlc_id = pkg_attrs[_DLC_ID]
986 logging.notice('Uninstalling DLC image for %s', dlc_id)
987
988 device.RunCommand(['sudo', '-u', 'chronos', 'dlcservice_util',
989 '--uninstall', '--dlc_ids=%s' % dlc_id])
990 return True
991 else:
992 logging.debug('DLC_ID not found in package')
993 return False
994
995
996def _DeployDLCImage(device, pkg_path):
997 """Deploy (install and mount) a DLC image."""
998 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
999 if dlc_id and dlc_package:
1000 logging.notice('Deploy a DLC image for %s', dlc_id)
1001
1002 dlc_path_src = os.path.join('/build/rootfs/dlc', dlc_id, dlc_package,
1003 'dlc.img')
1004 dlc_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1005 dlc_path_a = os.path.join(dlc_path, 'dlc_a')
1006 dlc_path_b = os.path.join(dlc_path, 'dlc_b')
1007 # Create folders for DLC images.
1008 device.RunCommand(['mkdir', '-p', dlc_path_a, dlc_path_b])
1009 # Copy images to the destination folders.
1010 device.RunCommand(['cp', dlc_path_src,
1011 os.path.join(dlc_path_a, 'dlc.img')])
1012 device.RunCommand(['cp', dlc_path_src,
1013 os.path.join(dlc_path_b, 'dlc.img')])
1014
1015 # Set the proper perms and ownership so dlcservice can access the image.
1016 device.RunCommand(['chmod', '-R', '0755', _DLC_INSTALL_ROOT])
1017 device.RunCommand(['chown', '-R', 'dlcservice:dlcservice',
1018 _DLC_INSTALL_ROOT])
1019 return True
1020 else:
1021 logging.debug('DLC_ID not found in package')
1022 return False
1023
1024
1025def _GetDLCInfo(device, pkg_path, from_dut):
1026 """Returns information of a DLC given its package path.
1027
1028 Args:
1029 device: commandline.Device object; None to use the default device.
1030 pkg_path: path to the package.
1031 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1032 info from host.
1033
1034 Returns:
1035 A tuple (dlc_id, dlc_package).
1036 """
1037 environment_content = ''
1038 if from_dut:
1039 # On DUT, |pkg_path| is the directory which contains environment file.
1040 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1041 result = device.RunCommand(['test', '-f', environment_path],
Woody Chowde57a322020-01-07 16:18:52 +09001042 check=False, encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001043 if result.returncode == 1:
1044 # The package is not installed on DUT yet. Skip extracting info.
1045 return None, None
Woody Chowde57a322020-01-07 16:18:52 +09001046 result = device.RunCommand(['bzip2', '-d', '-c', environment_path],
1047 encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001048 environment_content = result.output
1049 else:
1050 # On host, pkg_path is tbz2 file which contains environment file.
1051 # Extract the metadata of the package file.
1052 data = portage.xpak.tbz2(pkg_path).get_data()
1053 # Extract the environment metadata.
Woody Chowde57a322020-01-07 16:18:52 +09001054 environment_content = bz2.decompress(
Alex Klein9a1b3722020-01-28 09:59:35 -07001055 data[_ENVIRONMENT_FILENAME.encode('utf-8')])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001056
1057 with tempfile.NamedTemporaryFile() as f:
1058 # Dumps content into a file so we can use osutils.SourceEnvironment.
1059 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001060 osutils.WriteFile(path, environment_content, mode='wb')
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001061 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE))
1062 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001063
1064
Gilad Arnolda0a98062015-07-07 08:34:27 -07001065def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1066 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1067 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1068 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001069 """Deploys packages to a device.
1070
1071 Args:
David Pursell2e773382015-04-03 14:30:47 -07001072 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001073 packages: List of packages (strings) to deploy to device.
1074 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001075 emerge: True to emerge package, False to unmerge.
1076 update: Check installed version on device.
1077 deep: Install dependencies also. Implies |update|.
1078 deep_rev: Install reverse dependencies. Implies |deep|.
1079 clean_binpkg: Clean outdated binary packages.
1080 root: Package installation root path.
1081 strip: Run strip_package to filter out preset paths in the package.
1082 emerge_args: Extra arguments to pass to emerge.
1083 ssh_private_key: Path to an SSH private key file; None to use test keys.
1084 ping: True to ping the device before trying to connect.
1085 force: Ignore sanity checks and prompts.
1086 dry_run: Print deployment plan but do not deploy anything.
1087
1088 Raises:
1089 ValueError: Invalid parameter or parameter combination.
1090 DeployError: Unrecoverable failure during deploy.
1091 """
1092 if deep_rev:
1093 deep = True
1094 if deep:
1095 update = True
1096
Gilad Arnolda0a98062015-07-07 08:34:27 -07001097 if not packages:
1098 raise DeployError('No packages provided, nothing to deploy.')
1099
David Pursell9476bf42015-03-30 13:34:27 -07001100 if update and not emerge:
1101 raise ValueError('Cannot update and unmerge.')
1102
David Pursell2e773382015-04-03 14:30:47 -07001103 if device:
1104 hostname, username, port = device.hostname, device.username, device.port
1105 else:
1106 hostname, username, port = None, None, None
1107
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001108 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001109 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001110 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001111 # Somewhat confusing to clobber, but here we are.
1112 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001113 with remote_access.ChromiumOSDeviceHandler(
1114 hostname, port=port, username=username, private_key=ssh_private_key,
1115 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001116 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001117
Gilad Arnolda0a98062015-07-07 08:34:27 -07001118 board = cros_build_lib.GetBoard(device_board=device.board,
1119 override_board=board)
1120 if not force and board != device.board:
1121 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001122 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001123
Gilad Arnolda0a98062015-07-07 08:34:27 -07001124 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -07001125
1126 if clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001127 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001128 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001129
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001130 # Remount rootfs as writable if necessary.
1131 if not device.MountRootfsReadWrite():
1132 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001133
1134 # Obtain list of packages to upgrade/remove.
1135 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001136 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001137 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001138 if emerge:
1139 action_str = 'emerge'
1140 else:
1141 pkgs.reverse()
1142 action_str = 'unmerge'
1143
1144 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001145 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001146 return
1147
Ralph Nathane01ccf12015-04-16 10:40:32 -07001148 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001149 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001150 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001151
1152 if dry_run or not _ConfirmDeploy(num_updates):
1153 return
1154
Ralph Nathane01ccf12015-04-16 10:40:32 -07001155 # Select function (emerge or unmerge) and bind args.
1156 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001157 func = functools.partial(_EmergePackages, pkgs, device, strip,
Ralph Nathane01ccf12015-04-16 10:40:32 -07001158 sysroot, root, emerge_args)
1159 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001160 func = functools.partial(_UnmergePackages, pkgs, device, root,
1161 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001162
1163 # Call the function with the progress bar or with normal output.
1164 if command.UseProgressBar():
1165 op = BrilloDeployOperation(len(pkgs), emerge)
1166 op.Run(func, log_level=logging.DEBUG)
1167 else:
1168 func()
David Pursell9476bf42015-03-30 13:34:27 -07001169
Ben Pastene5f03b052019-08-12 18:03:24 -07001170 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001171 if sum(x.count('selinux-policy') for x in pkgs):
1172 logging.warning(
1173 'Deploying SELinux policy will not take effect until reboot. '
1174 'SELinux policy is loaded by init. Also, security contexts '
1175 '(labels) in files will require manual relabeling by the user '
1176 'if your policy modifies the file contexts.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001177
David Pursell9476bf42015-03-30 13:34:27 -07001178 logging.warning('Please restart any updated services on the device, '
1179 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001180 except Exception:
1181 if lsb_release:
1182 lsb_entries = sorted(lsb_release.items())
1183 logging.info('Following are the LSB version details of the device:\n%s',
1184 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1185 raise